1、修复业务模块bug

2、完善凭证管理
This commit is contained in:
2026-08-13 17:54:35 +08:00
parent 31dbd731dd
commit 323ce61136
20 changed files with 569 additions and 15 deletions
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 凭证导入消息队列配置。
* RabbitMQ 连接参数由 Nacos 的 spring.rabbitmq 配置提供。
*/
@Configuration
public class VoucherImportRabbitConfig {
public static final String EXCHANGE = "tms.voucher.import.exchange";
public static final String QUEUE = "tms.voucher.import.queue";
public static final String ROUTING_KEY = "tms.voucher.import";
@Bean
public DirectExchange voucherImportExchange() {
return new DirectExchange(EXCHANGE, true, false);
}
@Bean
public Queue voucherImportQueue() {
return new Queue(QUEUE, true);
}
@Bean
public Binding voucherImportBinding(Queue voucherImportQueue, DirectExchange voucherImportExchange) {
return BindingBuilder.bind(voucherImportQueue).to(voucherImportExchange).with(ROUTING_KEY);
}
}
@@ -0,0 +1,26 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 凭证图片 MinIO 客户端配置。
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
*/
@Configuration
public class VoucherMinioConfig {
@Bean
public MinioClient voucherMinioClient(
@Value("${file.storage.minio.endpoint:${minio.endpoint:}}") String endpoint,
@Value("${file.storage.minio.access-key-id:${minio.access-key:}}") String accessKey,
@Value("${file.storage.minio.access-key-secret:${minio.secret-key:}}") String secretKey) {
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
}
}
@@ -24,11 +24,13 @@ package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.http.Method;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.mp.support.Condition;
@@ -38,10 +40,13 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.excel.ProcessConfigExcel;
import org.springblade.transport.mapper.VoucherImageMapper;
import org.springblade.transport.pojo.entity.ProcessConfig;
import org.springblade.transport.pojo.entity.VoucherImage;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.service.IProcessConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -50,7 +55,10 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 过程配置 控制器
@@ -58,19 +66,64 @@ import java.util.List;
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "process_config")
@RequestMapping("/process-config")
@Tag(name = "过程配置", description = "过程配置")
public class ProcessConfigController extends BladeController {
private final IProcessConfigService processConfigService;
private final VoucherImageMapper voucherImageMapper;
private final MinioClient minioClient;
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, VoucherImageMapper voucherImageMapper,
MinioClient minioClient) {
this.processConfigService = processConfigService;
this.voucherImageMapper = voucherImageMapper;
this.minioClient = minioClient;
}
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "详情", description = "传入id")
public R<ProcessConfigVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(processConfigService.detail(id));
public R<ProcessConfigVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id,
@Parameter(description = "运单主键") @RequestParam(required = false) Long waybillId) {
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);
return R.data(detail);
}
@GetMapping("/voucher-images")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单已关联凭证图片")
public R<List<Map<String, Object>>> voucherImages(
@Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) {
List<Map<String, Object>> images = 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 -> {
Map<String, Object> result = new LinkedHashMap<>();
result.put("id", image.getId());
result.put("imageName", image.getImageName());
result.put("plateNo", image.getPlateNo());
result.put("waybillNo", image.getWaybillNo());
result.put("objectKey", image.getObjectKey());
try {
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
.expiry(1, TimeUnit.HOURS).build()));
} catch (Exception e) {
throw new IllegalStateException("生成凭证图片预览地址失败", e);
}
return result;
}).toList();
return R.data(images);
}
@GetMapping("/list")
@@ -65,6 +65,13 @@ public class VoucherManageController extends BladeController {
return R.success("文件已更新");
}
@PostMapping("/reprocess")
@Operation(summary = "重新处理已上传凭证")
public R reprocess(@RequestParam Long id) {
voucherManageService.reprocessUploadedVoucher(id);
return R.success("重新处理完成");
}
@PostMapping("/remove")
@ApiOperationSupport(order = 5)
@Operation(summary = "删除凭证")
@@ -177,36 +177,43 @@ public class WaybillController extends BladeController {
return R.data(waybillService.copy(id));
}
@PostMapping("/cancel")
@PostMapping("/change-route")
@ApiOperationSupport(order = 11)
@Operation(summary = "变更运输路线", description = "传入运单路线与变更记录")
public R changeRoute(@RequestBody Waybill waybill) {
return R.status(waybillService.changeRoute(waybill));
}
@PostMapping("/cancel")
@ApiOperationSupport(order = 12)
@Operation(summary = "取消", description = "传入id")
public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(waybillService.cancel(id));
}
@PostMapping("/reassign")
@ApiOperationSupport(order = 12)
@ApiOperationSupport(order = 13)
@Operation(summary = "重新派单", description = "传入id")
public R reassign(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(waybillService.reassign(id));
}
@PostMapping("/complete")
@ApiOperationSupport(order = 13)
@ApiOperationSupport(order = 14)
@Operation(summary = "完成", description = "传入id")
public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(waybillService.complete(id));
}
@PostMapping("/batch-complete")
@ApiOperationSupport(order = 14)
@ApiOperationSupport(order = 15)
@Operation(summary = "批量完成", description = "传入ids")
public R<BusinessRemoveResultVO> batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
return R.data(waybillService.batchComplete(ids));
}
@PostMapping("/road-loading")
@ApiOperationSupport(order = 15)
@ApiOperationSupport(order = 16)
@Operation(summary = "公路配载", description = "传入ids")
public R<LoadingManageVO> roadLoading(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
return R.data(waybillService.roadLoading(ids));
@@ -0,0 +1,24 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.event;
import org.springframework.context.ApplicationEvent;
/**
* 凭证压缩包上传完成事件。
*/
public class VoucherUploadCompletedEvent extends ApplicationEvent {
private final Long voucherId;
public VoucherUploadCompletedEvent(Long voucherId) {
super(voucherId);
this.voucherId = voucherId;
}
public Long getVoucherId() {
return voucherId;
}
}
@@ -0,0 +1,26 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.listener;
import lombok.RequiredArgsConstructor;
import org.springblade.transport.config.VoucherImportRabbitConfig;
import org.springblade.transport.service.IVoucherManageService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 凭证压缩包后台处理消费者。
*/
@Component
@RequiredArgsConstructor
public class VoucherImportMessageListener {
private final IVoucherManageService voucherManageService;
@RabbitListener(queues = VoucherImportRabbitConfig.QUEUE)
public void processVoucher(Long voucherId) {
voucherManageService.processUploadedVoucher(voucherId);
}
}
@@ -0,0 +1,29 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.listener;
import lombok.RequiredArgsConstructor;
import org.springblade.transport.config.VoucherImportRabbitConfig;
import org.springblade.transport.event.VoucherUploadCompletedEvent;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* 凭证上传完成后投递后台处理消息。
*/
@Component
@RequiredArgsConstructor
public class VoucherUploadCompletedListener {
private final RabbitTemplate rabbitTemplate;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void publish(VoucherUploadCompletedEvent event) {
rabbitTemplate.convertAndSend(VoucherImportRabbitConfig.EXCHANGE,
VoucherImportRabbitConfig.ROUTING_KEY, event.getVoucherId());
}
}
@@ -0,0 +1,19 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.entity.VoucherImage;
/**
* 凭证图片明细 Mapper。
*/
public interface VoucherImageMapper extends BaseMapper<VoucherImage> {
@Delete("DELETE FROM blade_voucher_image WHERE voucher_id = #{voucherId}")
void deleteByVoucherId(@Param("voucherId") Long voucherId);
}
@@ -2,9 +2,9 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.VoucherWaybillBatchMapper">
<select id="selectVoucherWaybillBatchPage" resultType="java.util.LinkedHashMap">
SELECT MAX(w.create_time) AS createTime, MAX(w.create_user) AS createUser,
SELECT MIN(w.id) AS id, MAX(w.create_time) AS createTime, MAX(w.create_user) AS createUser,
MAX(u.real_name) AS createUserName, COUNT(*) AS waybillCount,
COALESCE(NULLIF(w.batch_no, ''), '未分批运单') AS batchNo, GROUP_CONCAT(w.id ORDER BY w.create_time DESC) AS waybillIds
COALESCE(NULLIF(w.batch_no, ''), '未分批运单') AS batchNo
FROM blade_waybill w
LEFT JOIN blade_user u ON u.id = w.create_user
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId}
@@ -17,9 +17,10 @@
ORDER BY createTime DESC
</select>
<select id="selectWaybillBatchesByIds" resultType="java.util.LinkedHashMap">
SELECT w.id, COALESCE(NULLIF(w.batch_no, ''), '未分批运单') AS batchNo, 1 AS waybillCount
SELECT MIN(w.id) AS id, COALESCE(NULLIF(w.batch_no, ''), '未分批运单') AS batchNo, COUNT(*) AS waybillCount
FROM blade_waybill w
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId} AND w.id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
GROUP BY COALESCE(NULLIF(w.batch_no, ''), '未分批运单')
</select>
</mapper>
@@ -17,6 +17,8 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
void submit(VoucherManageSubmitRequest request);
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
void completeUploadFile(VoucherFileCompleteRequest request);
void processUploadedVoucher(Long voucherId);
void reprocessUploadedVoucher(Long voucherId);
void removeVoucher(Long id);
IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd);
}
@@ -46,6 +46,7 @@ public interface IWaybillService extends BaseService<Waybill> {
List<WaybillExcel> exportWaybill(WaybillVO waybill, String ids);
List<WaybillExcel> importWaybill(List<WaybillExcel> data);
WaybillVO copy(Long id);
boolean changeRoute(Waybill waybill);
boolean cancel(Long id);
boolean reassign(Long id);
boolean complete(Long id);
@@ -6,38 +6,76 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.Func;
import org.springblade.transport.mapper.VoucherManageMapper;
import org.springblade.transport.mapper.VoucherImageMapper;
import org.springblade.transport.mapper.VoucherWaybillBatchMapper;
import org.springblade.transport.event.VoucherUploadCompletedEvent;
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.VoucherManage;
import org.springblade.transport.pojo.entity.VoucherWaybillBatch;
import org.springblade.transport.pojo.entity.VoucherImage;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.vo.VoucherManageVO;
import org.springblade.transport.service.IVoucherManageService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.wrapper.VoucherManageWrapper;
import org.springframework.stereotype.Service;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Service
@Slf4j
public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMapper, VoucherManage> implements IVoucherManageService {
private final VoucherWaybillBatchMapper voucherWaybillBatchMapper;
private final IProjectApplyService projectApplyService;
private final IWaybillService waybillService;
private final VoucherImageMapper voucherImageMapper;
private final ApplicationEventPublisher eventPublisher;
private final MinioClient minioClient;
private final String minioBucketName;
private final String minioRootDirectory;
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService) {
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService,
VoucherImageMapper voucherImageMapper, ApplicationEventPublisher eventPublisher, MinioClient minioClient,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
this.projectApplyService = projectApplyService;
this.waybillService = waybillService;
this.voucherImageMapper = voucherImageMapper;
this.eventPublisher = eventPublisher;
this.minioClient = minioClient;
this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory;
}
@Override
@@ -114,10 +152,11 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
}
relations.forEach(voucherWaybillBatchMapper::insert);
voucher.setWaybillBatchNo(relations.stream().map(VoucherWaybillBatch::getWaybillBatchNo).distinct().collect(Collectors.joining(",")));
int total = relations.stream().mapToInt(VoucherWaybillBatch::getWaybillCount).sum();
voucher.setRelatedWaybillCount(total);
voucher.setRelatedWaybillCount(0);
voucher.setUnRelatedWaybillCount(0);
voucher.setProcessStatus("处理中");
updateById(voucher);
eventPublisher.publishEvent(new VoucherUploadCompletedEvent(voucher.getId()));
}
@Override
@@ -133,9 +172,96 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
voucher.setFileTaskId(request.getFileTaskId());
voucher.setFileName(request.getFileName());
voucher.setFileUrl(request.getFileUrl());
voucher.setProcessStatus("处理中");
updateById(voucher);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processUploadedVoucher(Long voucherId) {
processUploadedVoucher(voucherId, false);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void reprocessUploadedVoucher(Long voucherId) {
processUploadedVoucher(voucherId, true);
}
private void processUploadedVoucher(Long voucherId, boolean force) {
VoucherManage voucher = getById(voucherId);
if (voucher == null || Objects.equals(voucher.getIsDeleted(), 1)) {
log.warn("凭证解压任务不存在,忽略消息 voucherId:{}", voucherId);
return;
}
if (!force && "处理完成".equals(voucher.getProcessStatus())) {
return;
}
try {
validateMinioConfig();
List<Waybill> waybills = listRelatedWaybills(voucher);
Map<String, Waybill> waybillByPlate = new HashMap<>();
for (Waybill waybill : waybills) {
waybillPlateNumbers(waybill).forEach(plateNo -> waybillByPlate.putIfAbsent(plateNo, waybill));
}
voucherImageMapper.deleteByVoucherId(voucher.getId());
int imageCount = 0;
Set<Long> relatedWaybillIds = new HashSet<>();
try (InputStream source = openSourceFile(voucher.getFileUrl());
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), StandardCharsets.UTF_8)) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String[] pathParts = entry.getName().replace('\\', '/').split("/");
if (pathParts.length < 2 || !isImageFile(pathParts[pathParts.length - 1])) {
continue;
}
String plateNo = normalizePlateNo(pathParts[0]);
String imageName = safeFileName(pathParts[pathParts.length - 1]);
if (Func.isEmpty(plateNo) || Func.isEmpty(imageName)) {
continue;
}
Waybill waybill = waybillByPlate.get(plateNo);
String waybillNo = waybill == null ? "unmatched" : safePathPart(waybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, imageName);
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024).build());
VoucherImage image = new VoucherImage();
image.setVoucherId(voucher.getId());
image.setVoucherBatchNo(voucher.getVoucherBatchNo());
image.setWaybillId(waybill == null ? null : waybill.getId());
image.setWaybillNo(waybill == null ? null : waybill.getWaybillNo());
image.setPlateNo(plateNo);
image.setImageName(imageName);
image.setObjectKey(objectKey);
image.setMatched(waybill == null ? 0 : 1);
image.setTenantId(voucher.getTenantId());
voucherImageMapper.insert(image);
imageCount++;
if (waybill != null) {
relatedWaybillIds.add(waybill.getId());
}
}
}
VoucherManage update = new VoucherManage();
update.setId(voucher.getId());
update.setVoucherCount(imageCount);
update.setRelatedWaybillCount(relatedWaybillIds.size());
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
update.setProcessStatus("处理完成");
updateById(update);
} catch (Exception exception) {
VoucherManage update = new VoucherManage();
update.setId(voucher.getId());
update.setProcessStatus("处理失败");
updateById(update);
log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception);
throw new ServiceException("凭证压缩包处理失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeVoucher(Long id) {
@@ -155,6 +281,105 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids);
}
private List<Waybill> listRelatedWaybills(VoucherManage voucher) {
LambdaQueryWrapper<Waybill> query = Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getTenantId, voucher.getTenantId())
.eq(Waybill::getIsDeleted, 0);
List<String> batchNos = voucherWaybillBatchMapper.selectList(Wrappers.<VoucherWaybillBatch>lambdaQuery()
.eq(VoucherWaybillBatch::getVoucherId, voucher.getId()))
.stream().map(VoucherWaybillBatch::getWaybillBatchNo).filter(Func::isNotEmpty).distinct().toList();
if (Func.isEmpty(batchNos)) return List.of();
List<String> normalBatchNos = batchNos.stream().filter(batchNo -> !"未分批运单".equals(batchNo)).toList();
boolean containsUnbatchedWaybills = batchNos.contains("未分批运单");
query.and(wrapper -> {
if (Func.isNotEmpty(normalBatchNos)) {
wrapper.in(Waybill::getBatchNo, normalBatchNos);
}
if (containsUnbatchedWaybills) {
if (Func.isNotEmpty(normalBatchNos)) {
wrapper.or();
}
wrapper.isNull(Waybill::getBatchNo).or().eq(Waybill::getBatchNo, "");
}
});
return waybillService.list(query);
}
private InputStream openSourceFile(String fileUrl) throws Exception {
URLConnection connection = new java.net.URL(fileUrl).openConnection();
connection.setConnectTimeout(30_000);
connection.setReadTimeout(300_000);
return connection.getInputStream();
}
private void validateMinioConfig() {
if (Func.isEmpty(minioBucketName)) {
throw new ServiceException("Nacos 未配置 file.storage.minio.bucket-name");
}
}
private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String imageName) {
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + imageName;
if (Func.isEmpty(minioRootDirectory)) {
return objectKey;
}
return minioRootDirectory.endsWith("/") ? minioRootDirectory + objectKey : minioRootDirectory + "/" + objectKey;
}
private String normalizePlateNo(String plateNo) {
return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase();
}
private Set<String> waybillPlateNumbers(Waybill waybill) {
Set<String> plateNumbers = new HashSet<>();
addPlateNumber(plateNumbers, waybill.getVehicleNo());
addPlateNumber(plateNumbers, waybill.getTrailerVehicleNo());
addPlateNumbersFromJson(plateNumbers, waybill.getCarrierJson(), waybill.getId());
addPlateNumbersFromJson(plateNumbers, waybill.getTaskInfoJson(), waybill.getId());
return plateNumbers;
}
private void addPlateNumbersFromJson(Set<String> plateNumbers, String json, Long waybillId) {
if (Func.isEmpty(json)) return;
try {
Object parsed = JsonUtil.parse(json, Object.class);
if (parsed instanceof Map<?, ?> map) {
addPlateNumber(plateNumbers, String.valueOf(map.get("vehicleNo")));
addPlateNumber(plateNumbers, String.valueOf(map.get("trailerVehicleNo")));
} else if (parsed instanceof List<?> rows) {
for (Object row : rows) {
if (row instanceof Map<?, ?> map) {
addPlateNumber(plateNumbers, String.valueOf(map.get("vehicleNo")));
addPlateNumber(plateNumbers, String.valueOf(map.get("trailerVehicleNo")));
}
}
}
} catch (Exception exception) {
log.warn("运单车牌信息解析失败 waybillId:{}", waybillId);
}
}
private void addPlateNumber(Set<String> plateNumbers, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
if (Func.isNotEmpty(normalizedPlateNo) && !"NULL".equals(normalizedPlateNo)) {
plateNumbers.add(normalizedPlateNo);
}
}
private String safePathPart(String value) {
return value == null ? "" : value.replaceAll("[^0-9A-Za-z\\u4e00-\\u9fa5_-]", "_");
}
private String safeFileName(String value) {
return value == null ? "" : safePathPart(value.replaceFirst("(?s)^.*[/\\\\]", ""));
}
private boolean isImageFile(String fileName) {
String lowerName = fileName.toLowerCase();
return lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".png")
|| lowerName.endsWith(".bmp") || lowerName.endsWith(".webp");
}
private LambdaQueryWrapper<VoucherManage> buildQuery(VoucherManageVO query) {
LambdaQueryWrapper<VoucherManage> wrapper = Wrappers.<VoucherManage>lambdaQuery().eq(VoucherManage::getIsDeleted, 0)
.like(Func.isNotEmpty(query.getVoucherBatchNo()), VoucherManage::getVoucherBatchNo, query.getVoucherBatchNo())
@@ -224,6 +224,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
target.setCarrierJson(source.getCarrierJson());
target.setTaskInfoJson(source.getTaskInfoJson());
target.setProcessJson(source.getProcessJson());
target.setRouteJson(source.getRouteJson());
target.setFreightJson(source.getFreightJson());
target.setAttachmentsJson(source.getAttachmentsJson());
target.setRemark(source.getRemark());
@@ -235,6 +236,24 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
return detail(target.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean changeRoute(Waybill waybill) {
Waybill oldRecord = loadEditable(waybill.getId(), true);
if ("completed".equals(oldRecord.getBusinessStatus()) || "cancelled".equals(oldRecord.getBusinessStatus())) {
throw new ServiceException("当前运单状态不允许变更运输路线");
}
oldRecord.setRouteJson(TransportBusinessSupport.trimToNull(waybill.getRouteJson()));
oldRecord.setDepartureAddress(TransportBusinessSupport.trimToNull(waybill.getDepartureAddress()));
oldRecord.setArrivalAddress(TransportBusinessSupport.trimToNull(waybill.getArrivalAddress()));
oldRecord.setTaskInfoJson(TransportBusinessSupport.trimToNull(waybill.getTaskInfoJson()));
TransportBusinessSupport.validateLength(oldRecord.getRouteJson(), 8000, "路线信息不能超过8000字");
TransportBusinessSupport.validateLength(oldRecord.getDepartureAddress(), 255, "发货地址不能超过255字");
TransportBusinessSupport.validateLength(oldRecord.getArrivalAddress(), 255, "收货地址不能超过255字");
TransportBusinessSupport.validateLength(oldRecord.getTaskInfoJson(), 8000, "任务信息不能超过8000字");
return updateById(oldRecord);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancel(Long id) {
@@ -470,6 +489,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
waybill.setCarrierJson(TransportBusinessSupport.trimToNull(waybill.getCarrierJson()));
waybill.setTaskInfoJson(TransportBusinessSupport.trimToNull(waybill.getTaskInfoJson()));
waybill.setProcessJson(TransportBusinessSupport.trimToNull(waybill.getProcessJson()));
waybill.setRouteJson(TransportBusinessSupport.trimToNull(waybill.getRouteJson()));
waybill.setFreightJson(TransportBusinessSupport.trimToNull(waybill.getFreightJson()));
waybill.setAttachmentsJson(TransportBusinessSupport.trimToNull(waybill.getAttachmentsJson()));
waybill.setDeptName(TransportBusinessSupport.trimToNull(waybill.getDeptName()));
@@ -562,6 +582,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
TransportBusinessSupport.validateLength(waybill.getCarrierJson(), 8000, "承运信息不能超过8000字");
TransportBusinessSupport.validateLength(waybill.getTaskInfoJson(), 8000, "任务信息不能超过8000字");
TransportBusinessSupport.validateLength(waybill.getProcessJson(), 8000, "过程节点不能超过8000字");
TransportBusinessSupport.validateLength(waybill.getRouteJson(), 8000, "路线信息不能超过8000字");
TransportBusinessSupport.validateLength(waybill.getFreightJson(), 8000, "费用信息不能超过8000字");
TransportBusinessSupport.validateLength(waybill.getAttachmentsJson(), 8000, "附件不能超过8000字");
TransportBusinessSupport.validateLength(waybill.getDeptName(), 255, "所属组织不能超过255字");