diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java new file mode 100644 index 0000000..101fe2f --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherImage.java @@ -0,0 +1,35 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springblade.core.tenant.mp.TenantEntity; + +import java.io.Serial; + +/** + * 凭证图片明细。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("blade_voucher_image") +@Schema(description = "凭证图片明细") +public class VoucherImage extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + private Long voucherId; + private String voucherBatchNo; + private Long waybillId; + private String waybillNo; + private String plateNo; + private String imageName; + private String objectKey; + private Integer matched; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java index 656e041..cbd2b07 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/Waybill.java @@ -229,6 +229,9 @@ public class Waybill extends TenantEntity { @Schema(description = "过程节点") private String processJson; + @Schema(description = "路线信息") + private String routeJson; + @Schema(description = "费用信息") private String freightJson; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java index 4b267a0..424a568 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProcessConfigVO.java @@ -62,5 +62,8 @@ public class ProcessConfigVO extends ProcessConfig { @Schema(description = "状态名称") private String statusName; + @TableField(exist = false) + @Schema(description = "当前运单是否有关联凭证") + private Boolean hasRelatedVoucher; } diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml index c8853b4..852de9a 100644 --- a/blade-service/blade-transport/pom.xml +++ b/blade-service/blade-transport/pom.xml @@ -39,6 +39,14 @@ org.springblade blade-system-api + + org.springframework.boot + spring-boot-starter-amqp + + + io.minio + minio + diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java new file mode 100644 index 0000000..e784e56 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherImportRabbitConfig.java @@ -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); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java new file mode 100644 index 0000000..5c0545b --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/VoucherMinioConfig.java @@ -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(); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java index 12d2d3f..1bf3097 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java @@ -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 detail(@Parameter(description = "主键", required = true) @RequestParam Long id) { - return R.data(processConfigService.detail(id)); + public R 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() + .eq(VoucherImage::getWaybillId, waybillId) + .eq(VoucherImage::getMatched, 1)) > 0); + return R.data(detail); + } + + @GetMapping("/voucher-images") + @ApiOperationSupport(order = 2) + @Operation(summary = "查询运单已关联凭证图片") + public R>> voucherImages( + @Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) { + List> images = voucherImageMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherImage::getWaybillId, waybillId) + .eq(VoucherImage::getMatched, 1) + .orderByDesc(VoucherImage::getCreateTime)) + .stream().map(image -> { + Map 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") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java index b4d9ee7..dd98f3f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java @@ -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 = "删除凭证") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 53f83ad..3a749fe 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -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 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 roadLoading(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { return R.data(waybillService.roadLoading(ids)); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java new file mode 100644 index 0000000..3b4cbdd --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/event/VoucherUploadCompletedEvent.java @@ -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; + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java new file mode 100644 index 0000000..8b8f2bc --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherImportMessageListener.java @@ -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); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java new file mode 100644 index 0000000..792af9d --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/listener/VoucherUploadCompletedListener.java @@ -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()); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java new file mode 100644 index 0000000..2e8a516 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherImageMapper.java @@ -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 { + + @Delete("DELETE FROM blade_voucher_image WHERE voucher_id = #{voucherId}") + void deleteByVoucherId(@Param("voucherId") Long voucherId); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml index 0c726f6..624250b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml @@ -2,9 +2,9 @@ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java index be5d296..ead0e8b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java @@ -17,6 +17,8 @@ public interface IVoucherManageService extends BaseService { 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> selectableWaybillBatches(IPage page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java index 56bbeec..c73eb58 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java @@ -46,6 +46,7 @@ public interface IWaybillService extends BaseService { List exportWaybill(WaybillVO waybill, String ids); List importWaybill(List data); WaybillVO copy(Long id); + boolean changeRoute(Waybill waybill); boolean cancel(Long id); boolean reassign(Long id); boolean complete(Long id); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index 604dd91..0462940 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -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 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 waybills = listRelatedWaybills(voucher); + Map waybillByPlate = new HashMap<>(); + for (Waybill waybill : waybills) { + waybillPlateNumbers(waybill).forEach(plateNo -> waybillByPlate.putIfAbsent(plateNo, waybill)); + } + voucherImageMapper.deleteByVoucherId(voucher.getId()); + int imageCount = 0; + Set 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 listRelatedWaybills(VoucherManage voucher) { + LambdaQueryWrapper query = Wrappers.lambdaQuery() + .eq(Waybill::getTenantId, voucher.getTenantId()) + .eq(Waybill::getIsDeleted, 0); + List batchNos = voucherWaybillBatchMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherWaybillBatch::getVoucherId, voucher.getId())) + .stream().map(VoucherWaybillBatch::getWaybillBatchNo).filter(Func::isNotEmpty).distinct().toList(); + if (Func.isEmpty(batchNos)) return List.of(); + List 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 waybillPlateNumbers(Waybill waybill) { + Set 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 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 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 buildQuery(VoucherManageVO query) { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery().eq(VoucherManage::getIsDeleted, 0) .like(Func.isNotEmpty(query.getVoucherBatchNo()), VoucherManage::getVoucherBatchNo, query.getVoucherBatchNo()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 934c257..8d831c2 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -224,6 +224,7 @@ public class WaybillServiceImpl extends BaseServiceImpl 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 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 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 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字"); diff --git a/doc/sql/transport/blade_voucher_image_20260813.sql b/doc/sql/transport/blade_voucher_image_20260813.sql new file mode 100644 index 0000000..a43b095 --- /dev/null +++ b/doc/sql/transport/blade_voucher_image_20260813.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS `blade_voucher_image` ( + `id` bigint NOT NULL, + `tenant_id` varchar(12) NOT NULL DEFAULT '000000' COMMENT '租户ID', + `voucher_id` bigint NOT NULL COMMENT '凭证批次ID', + `voucher_batch_no` varchar(30) NOT NULL COMMENT '凭证批次号', + `waybill_id` bigint DEFAULT NULL COMMENT '关联运单ID', + `waybill_no` varchar(64) DEFAULT NULL COMMENT '关联运单号', + `plate_no` varchar(32) NOT NULL COMMENT '车牌号', + `image_name` varchar(255) NOT NULL COMMENT '图片名称', + `object_key` varchar(1000) NOT NULL COMMENT 'MinIO对象路径', + `matched` tinyint NOT NULL DEFAULT 0 COMMENT '是否匹配运单:0否、1是', + `create_user` bigint DEFAULT NULL, + `create_dept` bigint DEFAULT NULL, + `create_time` datetime DEFAULT NULL, + `update_user` bigint DEFAULT NULL, + `update_time` datetime DEFAULT NULL, + `status` tinyint NOT NULL DEFAULT 1, + `is_deleted` tinyint NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `idx_voucher_image_voucher` (`voucher_id`), + KEY `idx_voucher_image_waybill` (`waybill_id`), + KEY `idx_voucher_image_plate` (`tenant_id`, `plate_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证图片明细'; diff --git a/doc/sql/transport/blade_waybill_route_json_20260813.sql b/doc/sql/transport/blade_waybill_route_json_20260813.sql new file mode 100644 index 0000000..ac3e0d8 --- /dev/null +++ b/doc/sql/transport/blade_waybill_route_json_20260813.sql @@ -0,0 +1,2 @@ +ALTER TABLE `blade_waybill` + ADD COLUMN `route_json` text DEFAULT NULL COMMENT '路线信息' AFTER `process_json`;