Procházet zdrojové kódy

打印机查询信息

chenxiaofei před 1 měsícem
rodič
revize
15de45a5ff

+ 25 - 0
sckw-modules-api/sckw-fleet-api/src/main/java/com/sckw/fleet/api/feign/FleetTruckFeignService.java

@@ -0,0 +1,25 @@
+package com.sckw.fleet.api.feign;
+
+import com.sckw.core.web.response.HttpResult;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * 车辆档案 Feign 服务。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@FeignClient(name = "sckw-ng-fleet", contextId = "fleetTruckFeignService")
+public interface FleetTruckFeignService {
+
+    /**
+     * 根据车牌号查询车辆档案详情。
+     *
+     * @param truckNo 车牌号
+     * @return 车辆档案详情
+     */
+    @GetMapping("/kwfTruck/findByTruckNo")
+    HttpResult findByTruckNo(@RequestParam("truckNo") String truckNo);
+}

+ 7 - 1
sckw-modules/sckw-transport/pom.xml

@@ -153,6 +153,12 @@
             <artifactId>assertj-core</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>com.sckw</groupId>
             <artifactId>sckw-payment-api</artifactId>
@@ -179,4 +185,4 @@
             </plugin>
         </plugins>
     </build>
-</project>
+</project>

+ 49 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/KwtWaybillOrderController.java

@@ -21,6 +21,8 @@ import com.sckw.transport.api.model.param.AddLogisticOrderParam;
 import com.sckw.transport.dubbo.TransportServiceImpl;
 import com.sckw.transport.model.dto.*;
 import com.sckw.transport.model.dto.WaybillOrderSelectReq;
+import com.sckw.transport.model.param.TradeOrderTransportQueryReq;
+import com.sckw.transport.model.param.WaybillTransportQueryReq;
 import com.sckw.transport.model.param.WaybillOrderNodeReq;
 import com.sckw.transport.model.param.WaybillOrderReq;
 import com.sckw.transport.model.param.WaybillOrderResp;
@@ -28,6 +30,7 @@ import com.sckw.transport.model.vo.*;
 import com.sckw.transport.model.vo.WaybillOrderSelectOptionVo;
 import com.sckw.transport.service.KwtWaybillOrderService;
 import com.sckw.transport.service.KwtWaybillOrderV1Service;
+import com.sckw.transport.service.TradeOrderTransportInfoService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.http.HttpServletRequest;
@@ -60,6 +63,8 @@ public class KwtWaybillOrderController {
     KwtWaybillOrderV1Service waybillOrderV1Service;
     @Autowired
     TransportServiceImpl transportService;
+    @Autowired
+    TradeOrderTransportInfoService tradeOrderTransportInfoService;
 
     /**
      * @param params 请求参数
@@ -756,4 +761,48 @@ public class KwtWaybillOrderController {
 
         return BaseResult.success(waybillOrderService.getWaybillOrderSelectList(req));
     }
+
+    /**
+     * 根据贸易订单号查询运输相关信息。
+     *
+     * @param req 查询请求
+     * @return 运输相关信息
+     */
+    @PostMapping("/tradeOrder/transportInfo")
+    @Operation(summary = "根据贸易订单号查询运输相关信息")
+    public BaseResult<TradeOrderTransportInfoResp> queryTransportInfoByTradeOrderNo(@RequestBody @Valid TradeOrderTransportQueryReq req) {
+        try {
+            return BaseResult.success(tradeOrderTransportInfoService.queryByTradeOrderNo(req));
+        } catch (IllegalArgumentException e) {
+            log.warn("根据贸易订单号查询运输相关信息参数异常,tradeOrderNo:{}, errorMessage:{}",
+                    req == null ? null : req.getTradeOrderNo(), e.getMessage());
+            return BaseResult.failed(HttpStatus.PARAMETERS_MISSING_CODE, e.getMessage());
+        } catch (Exception e) {
+            log.error("根据贸易订单号查询运输相关信息失败,tradeOrderNo:{}, errorMessage:{}",
+                    req == null ? null : req.getTradeOrderNo(), e.getMessage(), e);
+            return BaseResult.failed(HttpStatus.GLOBAL_EXCEPTION_CODE, "查询运输相关信息失败");
+        }
+    }
+
+    /**
+     * 根据运单号查询运输相关信息。
+     *
+     * @param req 查询请求
+     * @return 运输相关信息
+     */
+    @PostMapping("/waybill/transportInfo")
+    @Operation(summary = "根据运单号查询运输相关信息")
+    public BaseResult<TradeOrderTransportInfoResp> queryTransportInfoByWaybillNo(@RequestBody @Valid WaybillTransportQueryReq req) {
+        try {
+            return BaseResult.success(tradeOrderTransportInfoService.queryByWaybillNo(req));
+        } catch (IllegalArgumentException e) {
+            log.warn("根据运单号查询运输相关信息参数异常,waybillNo:{}, errorMessage:{}",
+                    req == null ? null : req.getWaybillNo(), e.getMessage());
+            return BaseResult.failed(HttpStatus.PARAMETERS_MISSING_CODE, e.getMessage());
+        } catch (Exception e) {
+            log.error("根据运单号查询运输相关信息失败,waybillNo:{}, errorMessage:{}",
+                    req == null ? null : req.getWaybillNo(), e.getMessage(), e);
+            return BaseResult.failed(HttpStatus.GLOBAL_EXCEPTION_CODE, "查询运输相关信息失败");
+        }
+    }
 }

+ 29 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/model/param/TradeOrderTransportQueryReq.java

@@ -0,0 +1,29 @@
+package com.sckw.transport.model.param;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 贸易订单运输信息查询请求参数。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@Data
+@Schema(description = "贸易订单运输信息查询请求")
+public class TradeOrderTransportQueryReq implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 8365128408713487324L;
+
+    /**
+     * 贸易订单号。
+     */
+    @NotBlank(message = "贸易订单号不能为空")
+    @Schema(description = "贸易订单号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String tradeOrderNo;
+}

+ 29 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/model/param/WaybillTransportQueryReq.java

@@ -0,0 +1,29 @@
+package com.sckw.transport.model.param;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 运单运输信息查询请求参数。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@Data
+@Schema(description = "运单运输信息查询请求")
+public class WaybillTransportQueryReq implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 9050156770193841826L;
+
+    /**
+     * 运单号。
+     */
+    @NotBlank(message = "运单号不能为空")
+    @Schema(description = "运单号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String waybillNo;
+}

+ 160 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/model/vo/TradeOrderTransportInfoResp.java

@@ -0,0 +1,160 @@
+package com.sckw.transport.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 贸易订单运输信息响应。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@Data
+@Schema(description = "贸易订单运输信息响应")
+public class TradeOrderTransportInfoResp implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 2825054269070244593L;
+
+    @Schema(description = "贸易订单号")
+    private String tradeOrderNo;
+
+    @Schema(description = "供应企业名称")
+    private String supplierName;
+
+    @Schema(description = "客户名称")
+    private String customerName;
+
+    @Schema(description = "任务信息列表")
+    private List<TaskInfo> tasks = new ArrayList<>();
+
+    /**
+     * 单车任务信息。
+     */
+    @Data
+    @Schema(description = "单车任务信息")
+    public static class TaskInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 4264229244065846224L;
+
+        @Schema(description = "任务编号")
+        private String taskNo;
+
+        @Schema(description = "接单时间")
+        private String acceptTime;
+
+        @Schema(description = "完成时间")
+        private String finishTime;
+
+        @Schema(description = "计重人")
+        private String weigherName;
+
+        @Schema(description = "司机信息")
+        private DriverInfo driverInfo;
+
+        @Schema(description = "货物信息")
+        private GoodsInfo goodsInfo;
+
+        @Schema(description = "车辆信息")
+        private TruckInfo truckInfo;
+
+        @Schema(description = "目的地")
+        private String destination;
+
+        @Schema(description = "打印信息")
+        private PrintInfo printInfo;
+    }
+
+    /**
+     * 司机信息。
+     */
+    @Data
+    @Schema(description = "司机信息")
+    public static class DriverInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 4681187619812752711L;
+
+        @Schema(description = "姓名")
+        private String name;
+
+        @Schema(description = "手机号")
+        private String phone;
+
+        @Schema(description = "脱敏身份证号")
+        private String idCard;
+    }
+
+    /**
+     * 任务货物信息。
+     */
+    @Data
+    @Schema(description = "任务货物信息")
+    public static class GoodsInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 7576326426794710595L;
+
+        @Schema(description = "物料")
+        private String materialName;
+
+        @Schema(description = "规格")
+        private String specification;
+
+        @Schema(description = "任务量")
+        private BigDecimal taskAmount;
+
+        @Schema(description = "皮重")
+        private BigDecimal tareWeight;
+
+        @Schema(description = "毛重")
+        private BigDecimal grossWeight;
+
+        @Schema(description = "净重")
+        private BigDecimal netWeight;
+
+        @Schema(description = "单位")
+        private String unit;
+    }
+
+    /**
+     * 车辆信息。
+     */
+    @Data
+    @Schema(description = "车辆信息")
+    public static class TruckInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 4193011453340089506L;
+
+        @Schema(description = "车牌号")
+        private String truckNo;
+
+        @Schema(description = "车辆轴数")
+        private String truckAxle;
+    }
+
+    /**
+     * 打印信息。
+     */
+    @Data
+    @Schema(description = "打印信息")
+    public static class PrintInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = -6068786676826461870L;
+
+        @Schema(description = "打印时间")
+        private String printTime;
+
+        @Schema(description = "打印次数")
+        private Integer printCount;
+    }
+}

+ 14 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/repository/KwtLogisticsOrderRepository.java

@@ -163,4 +163,18 @@ public class KwtLogisticsOrderRepository extends ServiceImpl<KwtLogisticsOrderMa
                 .eq(KwtLogisticsOrder::getDelFlag,0)
                 .like(KwtLogisticsOrder::getTOrderNo, tradeOrderNo));
     }
+
+    /**
+     * 根据贸易订单号精确查询物流订单。
+     *
+     * @param tradeOrderNo 贸易订单号
+     * @return 物流订单列表
+     */
+    public List<KwtLogisticsOrder> queryByTradeOrderNo(String tradeOrderNo) {
+        return list(Wrappers.<KwtLogisticsOrder>lambdaQuery()
+                .eq(KwtLogisticsOrder::getDelFlag, 0)
+                .eq(KwtLogisticsOrder::getTOrderNo, tradeOrderNo)
+                .orderByDesc(KwtLogisticsOrder::getCreateTime)
+                .orderByDesc(KwtLogisticsOrder::getId));
+    }
 }

+ 885 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/TradeOrderTransportInfoService.java

@@ -0,0 +1,885 @@
+package com.sckw.transport.service;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.response.HttpResult;
+import com.sckw.fleet.api.feign.FleetTruckFeignService;
+import com.sckw.order.api.feign.TradeOrderApi;
+import com.sckw.order.api.model.OrderPara;
+import com.sckw.order.api.model.TradeOrderVo;
+import com.sckw.transport.model.KwtLogisticsOrder;
+import com.sckw.transport.model.KwtLogisticsOrderAddress;
+import com.sckw.transport.model.KwtLogisticsOrderGoods;
+import com.sckw.transport.model.KwtWaybillOrder;
+import com.sckw.transport.model.KwtWaybillOrderSubtask;
+import com.sckw.transport.model.KwtWaybillOrderTicket;
+import com.sckw.transport.model.param.TradeOrderTransportQueryReq;
+import com.sckw.transport.model.param.WaybillTransportQueryReq;
+import com.sckw.transport.model.vo.TradeOrderTransportInfoResp;
+import com.sckw.transport.repository.KwtLogisticsOrderAddressRepository;
+import com.sckw.transport.repository.KwtLogisticsOrderGoodsRepository;
+import com.sckw.transport.repository.KwtLogisticsOrderRepository;
+import com.sckw.transport.repository.KwtWaybillOrderRepository;
+import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
+import com.sckw.transport.repository.KwtWaybillOrderTicketRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 贸易订单运输信息查询服务。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TradeOrderTransportInfoService {
+
+    private static final int LOAD_TICKET_TYPE = 1;
+    private static final int UNLOAD_ADDRESS_TYPE = 2;
+    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+    private final TradeOrderApi tradeOrderApi;
+    private final FleetTruckFeignService fleetTruckFeignService;
+    private final KwtLogisticsOrderRepository logisticsOrderRepository;
+    private final KwtWaybillOrderRepository waybillOrderRepository;
+    private final KwtWaybillOrderSubtaskRepository waybillOrderSubtaskRepository;
+    private final KwtWaybillOrderTicketRepository waybillOrderTicketRepository;
+    private final KwtLogisticsOrderGoodsRepository logisticsOrderGoodsRepository;
+    private final KwtLogisticsOrderAddressRepository logisticsOrderAddressRepository;
+
+
+    /**
+     * 根据贸易订单号查询运输相关信息。
+     * <p>
+     * 该方法通过贸易订单号串联查询物流订单、运单及关联的运输任务详情,组装成完整的运输信息响应对象。
+     * 主要流程:
+     * 1. 校验入参及贸易订单号有效性。
+     * 2. 查询贸易订单基础信息(用于补充买卖双方企业名称等)。
+     * 3. 根据贸易订单号查询关联的物流订单列表,若不存在则抛出异常。
+     * 4. 提取物流订单ID,批量查询关联的运单(运输任务)列表,若不存在则抛出异常。
+     * 5. 组装并返回包含任务、货物、车辆、司机等信息的响应对象。
+     * </p>
+     *
+     * @param req 查询参数,包含贸易订单号
+     * @return 贸易订单运输信息响应对象
+     * @throws IllegalArgumentException 当参数为空、贸易订单号为空、未查询到物流订单或未查询到运单时抛出
+     */
+    public TradeOrderTransportInfoResp queryByTradeOrderNo(TradeOrderTransportQueryReq req) {
+        // 1. 参数校验
+        if (Objects.isNull(req)) {
+            log.warn("查询贸易订单运输信息失败:查询参数为空");
+            throw new IllegalArgumentException("查询参数不能为空");
+        }
+        String tradeOrderNo = StringUtils.trimToEmpty(req.getTradeOrderNo());
+        if (StringUtils.isBlank(tradeOrderNo)) {
+            log.warn("查询贸易订单运输信息失败:贸易订单号为空");
+            throw new IllegalArgumentException("贸易订单号不能为空");
+        }
+        log.info("查询贸易订单运输信息开始,tradeOrderNo:{}", tradeOrderNo);
+
+        // 2. 查询贸易订单基础信息
+        TradeOrderVo tradeOrder = queryTradeOrder(tradeOrderNo);
+        
+        // 3. 查询关联的物流订单列表
+        List<KwtLogisticsOrder> logisticsOrders = queryLogisticsOrders(tradeOrderNo, tradeOrder);
+        if (CollectionUtils.isEmpty(logisticsOrders)) {
+            log.warn("查询贸易订单运输信息失败:未查询到关联的物流订单,tradeOrderNo:{}", tradeOrderNo);
+            throw new IllegalArgumentException("未查询到贸易订单关联的物流订单");
+        }
+        log.debug("查询到关联物流订单数量:{}", logisticsOrders.size());
+
+        // 4. 提取物流订单ID并查询关联的运单(运输任务)列表
+        List<Long> logisticsOrderIds = logisticsOrders.stream()
+                .map(KwtLogisticsOrder::getId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        
+        List<KwtWaybillOrder> waybillOrders = queryWaybillOrders(logisticsOrderIds);
+        if (CollectionUtils.isEmpty(waybillOrders)) {
+            log.warn("查询贸易订单运输信息失败:未查询到关联的运输任务,tradeOrderNo:{}, logisticsOrderIds:{}", 
+                    tradeOrderNo, logisticsOrderIds);
+            throw new IllegalArgumentException("未查询到贸易订单关联的运输任务");
+        }
+        log.debug("查询到关联运单数量:{}", waybillOrders.size());
+
+        // 5. 组装响应数据
+        TradeOrderTransportInfoResp resp = buildTransportInfo(logisticsOrders, waybillOrders, tradeOrder);
+        resp.setTradeOrderNo(tradeOrderNo);
+        
+        log.info("查询贸易订单运输信息完成,tradeOrderNo:{}, taskSize:{}", tradeOrderNo, 
+                Objects.nonNull(resp.getTasks()) ? resp.getTasks().size() : 0);
+        return resp;
+    }
+
+    /**
+     * 查询贸易订单基础信息。
+     *
+     * @param tradeOrderNo 贸易订单号
+     * @return 贸易订单基础信息
+     */
+    /**
+     * 根据运单号查询运输相关信息。
+     *
+     * @param req 查询参数
+     * @return 运单运输信息
+     */
+    /**
+     * 根据运单号查询运输相关信息。
+     * <p>
+     * 该方法通过运单号反向追溯关联的物流订单及贸易订单,组装完整的运输链路信息。
+     * 主要流程:
+     * 1. 校验入参及运单号有效性。
+     * 2. 查询运单基础信息,校验是否存在及是否关联物流订单。
+     * 3. 根据运单关联的物流订单ID,查询物流订单详情。
+     * 4. 若物流订单中存在贸易订单号,则进一步查询贸易订单详细信息(如买卖双方)。
+     * 5. 组装并返回包含任务、货物、车辆、司机等信息的响应对象。
+     * </p>
+     *
+     * @param req 查询参数,包含运单号
+     * @return 贸易订单运输信息响应对象(复用同一响应结构)
+     * @throws IllegalArgumentException 当参数为空、运单不存在、未关联物流订单或物流订单不存在时抛出
+     */
+    public TradeOrderTransportInfoResp queryByWaybillNo(WaybillTransportQueryReq req) {
+        log.info("根据运单号查询运输信息开始,请求参数:{}", JSON.toJSONString( req));
+        // 1. 参数校验
+        if (Objects.isNull(req)) {
+            log.warn("根据运单号查询运输信息失败:查询参数为空");
+            throw new IllegalArgumentException("查询参数不能为空");
+        }
+        String waybillNo = StringUtils.trimToEmpty(req.getWaybillNo());
+        if (StringUtils.isBlank(waybillNo)) {
+            log.warn("根据运单号查询运输信息失败:运单号为空");
+            throw new IllegalArgumentException("运单号不能为空");
+        }
+        log.info("根据运单号查询运输信息开始,waybillNo:{}", waybillNo);
+
+        // 2. 查询运单信息
+        KwtWaybillOrder waybillOrder = waybillOrderRepository.queryByWayOrderNo(waybillNo);
+        if (Objects.isNull(waybillOrder)) {
+            log.warn("根据运单号查询运输信息失败:未查询到运单信息,waybillNo:{}", waybillNo);
+            throw new IllegalArgumentException("未查询到运单信息");
+        }
+        
+        // 3. 校验运单是否关联物流订单
+        if (Objects.isNull(waybillOrder.getLOrderId())) {
+            log.warn("根据运单号查询运输信息失败:运单未关联物流订单,waybillNo:{}, waybillId:{}", waybillNo, waybillOrder.getId());
+            throw new IllegalArgumentException("运单未关联物流订单");
+        }
+
+        // 4. 查询关联的物流订单
+        KwtLogisticsOrder logisticsOrder = logisticsOrderRepository.queryByLogisticsOrderId(waybillOrder.getLOrderId());
+        if (Objects.isNull(logisticsOrder)) {
+            log.error("根据运单号查询运输信息失败:未查询到运单关联的物流订单,waybillNo:{}, logisticsOrderId:{}", 
+                    waybillNo, waybillOrder.getLOrderId());
+            throw new IllegalArgumentException("未查询到运单关联的物流订单");
+        }
+        log.debug("查询到关联物流订单,logisticsOrderId:{}, tradeOrderNo:{}", logisticsOrder.getId(), logisticsOrder.getTOrderNo());
+
+        // 5. 查询贸易订单信息(可选,用于补充买卖双方企业名称等)
+        TradeOrderVo tradeOrder = null;
+        if (StringUtils.isNotBlank(logisticsOrder.getTOrderNo())) {
+            try {
+                tradeOrder = queryTradeOrder(logisticsOrder.getTOrderNo());
+                if (Objects.nonNull(tradeOrder)) {
+                    log.debug("查询到关联贸易订单,tradeOrderNo:{}", tradeOrder.getTOrderNo());
+                } else {
+                    log.warn("物流订单关联了贸易订单号但未查询到详细信息,tradeOrderNo:{}", logisticsOrder.getTOrderNo());
+                }
+            } catch (Exception e) {
+                log.error("查询贸易订单异常,但不影响主流程,tradeOrderNo:{}", logisticsOrder.getTOrderNo(), e);
+            }
+        }
+
+        // 6. 组装响应数据
+        TradeOrderTransportInfoResp resp = buildTransportInfo(
+                Collections.singletonList(logisticsOrder),
+                Collections.singletonList(waybillOrder),
+                tradeOrder
+        );
+        
+        log.info("根据运单号查询运输信息完成,waybillNo:{}, taskSize:{}", waybillNo, 
+                Objects.nonNull(resp.getTasks()) ? resp.getTasks().size() : 0);
+        return resp;
+    }
+
+    /**
+     * 查询贸易订单详细信息。
+     * <p>
+     * 通过 Feign 调用订单服务接口,根据贸易订单号获取对应的贸易订单数据。
+     * 若返回多个订单,优先匹配订单号完全一致的记录;若无完全匹配,则返回列表中的第一个订单作为兜底。
+     * </p>
+     *
+     * @param tradeOrderNo 贸易订单号
+     * @return 贸易订单视图对象,若未查询到或发生异常则返回 null
+     */
+    private TradeOrderVo queryTradeOrder(String tradeOrderNo) {
+        log.debug("开始查询贸易订单详情,tradeOrderNo:{}", tradeOrderNo);
+        try {
+            // 构建查询参数
+            OrderPara para = new OrderPara();
+            para.setOrderNo(tradeOrderNo);
+            
+            // 调用远程订单服务接口
+            List<TradeOrderVo> tradeOrders = tradeOrderApi.trade(para);
+            
+            if (CollectionUtils.isEmpty(tradeOrders)) {
+                log.warn("远程服务返回的贸易订单列表为空,tradeOrderNo:{}", tradeOrderNo);
+                return null;
+            }
+
+            // 优先精确匹配订单号
+            Optional<TradeOrderVo> exactMatch = tradeOrders.stream()
+                    .filter(item -> tradeOrderNo.equals(item.getTOrderNo()))
+                    .findFirst();
+            
+            if (exactMatch.isPresent()) {
+                log.debug("精确匹配到贸易订单,tradeOrderNo:{}", tradeOrderNo);
+                return exactMatch.get();
+            }
+
+            // 若无精确匹配,取列表第一个作为兜底(兼容历史数据或特殊场景)
+            TradeOrderVo fallbackOrder = tradeOrders.get(0);
+            log.warn("未精确匹配到贸易订单号,使用返回列表中的第一个订单作为兜底,requestNo:{}, actualOrderNo:{}", 
+                    tradeOrderNo, fallbackOrder.getTOrderNo());
+            return fallbackOrder;
+            
+        } catch (Exception e) {
+            log.error("Feign 调用订单服务查询贸易订单异常,tradeOrderNo:{}, errorMessage:{}", 
+                    tradeOrderNo, e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 查询物流订单,优先使用本地贸易订单号精确匹配。
+     *
+     * @param tradeOrderNo 贸易订单号
+     * @param tradeOrder   远程贸易订单
+     * @return 物流订单列表
+     */
+    /**
+     * 查询物流订单列表。
+     * <p>
+     * 查询策略采用“优先单号匹配,降级ID匹配”的方式:
+     * 1. 首先尝试通过贸易订单号(tradeOrderNo)在本地数据库查询物流订单。
+     * 2. 如果查询结果为空,且提供了有效的贸易订单对象及其ID(tOrderId),则降级使用贸易订单ID进行查询。
+     * 3. 这种设计兼容了不同数据来源或历史数据中订单号不一致但ID关联的场景。
+     * </p>
+     *
+     * @param tradeOrderNo 贸易订单号
+     * @param tradeOrder   贸易订单详细信息,用于获取贸易订单ID作为兜底查询条件
+     * @return 物流订单列表,若未查询到则返回空列表
+     */
+    private List<KwtLogisticsOrder> queryLogisticsOrders(String tradeOrderNo, TradeOrderVo tradeOrder) {
+        log.debug("开始查询物流订单,tradeOrderNo:{}", tradeOrderNo);
+        
+        // 第一步:优先通过贸易订单号查询
+        List<KwtLogisticsOrder> logisticsOrders = logisticsOrderRepository.queryByTradeOrderNo(tradeOrderNo);
+        
+        // 如果查询到了结果,或者无法进行降级查询(贸易订单为空或缺少ID),则直接返回当前结果
+        if (CollectionUtils.isNotEmpty(logisticsOrders)) {
+            log.debug("通过贸易订单号查询到物流订单,count:{}", logisticsOrders.size());
+            return logisticsOrders;
+        }
+        
+        if (Objects.isNull(tradeOrder) || Objects.isNull(tradeOrder.getTOrderId())) {
+            log.warn("通过贸易订单号未查询到物流订单,且无法通过贸易订单ID降级查询(tradeOrder为空或tOrderId为空)");
+            return Collections.emptyList();
+        }
+
+        // 第二步:降级通过贸易订单ID查询
+        log.info("通过贸易订单号未查询到物流订单,尝试通过贸易订单ID降级查询,tOrderId:{}", tradeOrder.getTOrderId());
+        List<KwtLogisticsOrder> fallbackOrders = logisticsOrderRepository.queryByTradeOrderId(tradeOrder.getTOrderId());
+        
+        if (CollectionUtils.isNotEmpty(fallbackOrders)) {
+            log.info("通过贸易订单ID成功查询到物流订单,count:{}", fallbackOrders.size());
+        } else {
+            log.warn("通过贸易订单ID也未查询到物流订单,tOrderId:{}", tradeOrder.getTOrderId());
+        }
+        
+        return fallbackOrders;
+    }
+
+    /**
+     * 查询运单列表。
+     *
+     * @param logisticsOrderIds 物流订单ID列表
+     * @return 运单列表
+     */
+    private List<KwtWaybillOrder> queryWaybillOrders(List<Long> logisticsOrderIds) {
+        if (CollectionUtils.isEmpty(logisticsOrderIds)) {
+            return Collections.emptyList();
+        }
+        return waybillOrderRepository.list(Wrappers.<KwtWaybillOrder>lambdaQuery()
+                .eq(KwtWaybillOrder::getDelFlag, 0)
+                .in(KwtWaybillOrder::getLOrderId, logisticsOrderIds)
+                .orderByDesc(KwtWaybillOrder::getTaskEndTime)
+                .orderByDesc(KwtWaybillOrder::getUpdateTime)
+                .orderByDesc(KwtWaybillOrder::getId));
+    }
+
+    /**
+     * 填充贸易订单企业信息。
+     *
+     * @param resp           响应对象
+     * @param tradeOrder     远程贸易订单
+     * @param logisticsOrder 本地物流订单
+     */
+    private void fillTradeOrderInfo(TradeOrderTransportInfoResp resp, TradeOrderVo tradeOrder, KwtLogisticsOrder logisticsOrder) {
+        resp.setSupplierName(Objects.nonNull(tradeOrder) ? tradeOrder.getSellEntName() : null);
+        resp.setCustomerName(Objects.nonNull(tradeOrder) ? tradeOrder.getBuyEntName() : null);
+        if (StringUtils.isBlank(resp.getTradeOrderNo()) && Objects.nonNull(logisticsOrder)) {
+            resp.setTradeOrderNo(logisticsOrder.getTOrderNo());
+        }
+    }
+
+    /**
+     * 组装任务信息列表。
+     *
+     * @param logisticsOrders 物流订单列表
+     * @param waybillOrders   运单列表
+     * @return 任务信息列表
+     */
+    /**
+     * 组装运输信息响应。
+     *
+     * @param logisticsOrders 物流订单列表
+     * @param waybillOrders   运单列表
+     * @param tradeOrder      贸易订单信息
+     * @return 运输信息响应
+     */
+    private TradeOrderTransportInfoResp buildTransportInfo(List<KwtLogisticsOrder> logisticsOrders,
+                                                           List<KwtWaybillOrder> waybillOrders,
+                                                           TradeOrderVo tradeOrder) {
+        TradeOrderTransportInfoResp resp = new TradeOrderTransportInfoResp();
+        KwtLogisticsOrder firstLogisticsOrder = CollectionUtils.isEmpty(logisticsOrders) ? null : logisticsOrders.get(0);
+        fillTradeOrderInfo(resp, tradeOrder, firstLogisticsOrder);
+        resp.setTasks(buildTaskInfoList(logisticsOrders, waybillOrders));
+        return resp;
+    }
+
+    /**
+     * 组装运输任务信息列表。
+     * <p>
+     * 该方法负责将物流订单和运单数据转换为前端展示的任务信息列表。
+     * 主要步骤包括:
+     * 1. 构建物流订单映射,以便快速通过ID获取订单详情。
+     * 2. 提取有效的物流订单ID和运单ID列表。
+     * 3. 批量查询并构建子任务、装货单、货物信息、卸货地址的映射关系,优化查询性能。
+     * 4. 遍历运单列表,结合上述映射数据组装每个任务的详细信息。
+     * </p>
+     *
+     * @param logisticsOrders 物流订单列表
+     * @param waybillOrders   运单列表
+     * @return 运输任务信息列表
+     */
+    private List<TradeOrderTransportInfoResp.TaskInfo> buildTaskInfoList(List<KwtLogisticsOrder> logisticsOrders,
+                                                                         List<KwtWaybillOrder> waybillOrders) {
+        log.debug("开始组装运输任务信息列表,logisticsOrderCount:{}, waybillOrderCount:{}", 
+                CollectionUtils.isEmpty(logisticsOrders) ? 0 : logisticsOrders.size(),
+                CollectionUtils.isEmpty(waybillOrders) ? 0 : waybillOrders.size());
+
+        // 1. 构建物流订单ID到对象的映射,处理可能的重复ID(保留第一个)
+        Map<Long, KwtLogisticsOrder> logisticsOrderMap = logisticsOrders.stream()
+                .filter(item -> Objects.nonNull(item.getId()))
+                .collect(Collectors.toMap(KwtLogisticsOrder::getId, Function.identity(), (a, b) -> a));
+        
+        // 2. 提取有效的ID列表用于后续批量查询
+        List<Long> logisticsOrderIds = logisticsOrders.stream()
+                .map(KwtLogisticsOrder::getId)
+                .filter(Objects::nonNull)
+                .toList();
+        List<Long> waybillOrderIds = waybillOrders.stream()
+                .map(KwtWaybillOrder::getId)
+                .filter(Objects::nonNull)
+                .toList();
+
+        if (CollectionUtils.isEmpty(waybillOrderIds)) {
+            log.warn("运单ID列表为空,无法组装任务信息");
+            return Collections.emptyList();
+        }
+
+        // 3. 批量查询关联数据并构建映射,以优化数据库交互次数
+        
+        // 查询运单子任务,若有多个子任务则选择最新更新的记录
+        Map<Long, KwtWaybillOrderSubtask> subtaskMap = waybillOrderSubtaskRepository.queryByWOrderIds(waybillOrderIds).stream()
+                .collect(Collectors.toMap(KwtWaybillOrderSubtask::getWOrderId, Function.identity(), this::chooseLatestSubtask));
+        log.debug("查询到运单子任务数量:{}", subtaskMap.size());
+
+        // 查询装货单(类型为LOAD_TICKET_TYPE),若有多个则选择最新更新的记录
+        Map<Long, KwtWaybillOrderTicket> loadTicketMap = waybillOrderTicketRepository.queryByWOrderIdsAndType(waybillOrderIds, LOAD_TICKET_TYPE).stream()
+                .collect(Collectors.toMap(KwtWaybillOrderTicket::getWOrderId, Function.identity(), this::chooseLatestTicket));
+        log.debug("查询到装货单数量:{}", loadTicketMap.size());
+
+        // 查询物流订单货物信息
+        Map<Long, KwtLogisticsOrderGoods> goodsMap = logisticsOrderGoodsRepository.queryByLogOrderIds(logisticsOrderIds).stream()
+                .collect(Collectors.toMap(KwtLogisticsOrderGoods::getLOrderId, Function.identity(), (a, b) -> a));
+        log.debug("查询到货物信息数量:{}", goodsMap.size());
+
+        // 查询卸货地址(类型为UNLOAD_ADDRESS_TYPE)
+        Map<Long, KwtLogisticsOrderAddress> unloadAddressMap = logisticsOrderAddressRepository.queryByLogOrderIds(logisticsOrderIds).stream()
+                .filter(item -> Objects.equals(UNLOAD_ADDRESS_TYPE, item.getAddressType()))
+                .collect(Collectors.toMap(KwtLogisticsOrderAddress::getLOrderId, Function.identity(), (a, b) -> a));
+        log.debug("查询到卸货地址数量:{}", unloadAddressMap.size());
+
+        // 用于缓存车辆轴数信息,避免重复Feign调用
+        Map<String, String> truckAxleCache = new LinkedHashMap<>();
+
+        // 4. 遍历运单,组装每个任务的详细信息
+        List<TradeOrderTransportInfoResp.TaskInfo> taskInfoList = waybillOrders.stream()
+                .map(waybillOrder -> buildTaskInfo(waybillOrder,
+                        logisticsOrderMap.get(waybillOrder.getLOrderId()),
+                        subtaskMap.get(waybillOrder.getId()),
+                        loadTicketMap.get(waybillOrder.getId()),
+                        goodsMap.get(waybillOrder.getLOrderId()),
+                        unloadAddressMap.get(waybillOrder.getLOrderId()),
+                        truckAxleCache))
+                .collect(Collectors.toList());
+        
+        log.info("运输任务信息列表组装完成,最终任务数量:{}", taskInfoList.size());
+        return taskInfoList;
+    }
+
+    /**
+     * 组装单车任务信息。
+     */
+    /**
+     * 组装单车任务详细信息。
+     * <p>
+     * 该方法将运单、物流订单、子任务、装货单、货物信息及卸货地址等分散的数据源,
+     * 整合为一个完整的任务信息对象(TaskInfo),用于前端展示单个运输任务的详细状态。
+     * </p>
+     *
+     * @param waybillOrder   运单主表信息,包含任务号、司机、车辆及基础时间信息
+     * @param logisticsOrder 关联的物流订单信息,用于补充货物基础单位及重量兜底
+     * @param subtask        运单子任务信息,包含实际装卸时间、委托量等执行细节
+     * @param loadTicket     装货单信息,包含皮重、毛重、净重及打印/上传时间
+     * @param goods          物流订单货物明细,包含货物名称、规格等静态属性
+     * @param unloadAddress  卸货地址信息,用于获取目的地城市名称
+     * @param truckAxleCache 车辆轴数缓存Map,Key为车牌号,Value为轴数,避免重复远程调用
+     * @return 组装好的单车任务信息对象
+     */
+    private TradeOrderTransportInfoResp.TaskInfo buildTaskInfo(KwtWaybillOrder waybillOrder,
+                                                               KwtLogisticsOrder logisticsOrder,
+                                                               KwtWaybillOrderSubtask subtask,
+                                                               KwtWaybillOrderTicket loadTicket,
+                                                               KwtLogisticsOrderGoods goods,
+                                                               KwtLogisticsOrderAddress unloadAddress,
+                                                               Map<String, String> truckAxleCache) {
+        log.debug("开始组装单车任务信息,waybillNo:{}", Objects.nonNull(waybillOrder) ? waybillOrder.getWOrderNo() : null);
+        
+        TradeOrderTransportInfoResp.TaskInfo taskInfo = new TradeOrderTransportInfoResp.TaskInfo();
+        
+        // 1. 设置任务编号
+        if (Objects.nonNull(waybillOrder)) {
+            taskInfo.setTaskNo(waybillOrder.getWOrderNo());
+            log.debug("设置任务编号: {}", waybillOrder.getWOrderNo());
+        }
+
+        // 2. 设置接单时间:优先使用任务开始时间,若为空则降级使用创建时间
+        Date acceptTime = firstNotNull(
+                Objects.nonNull(waybillOrder) ? waybillOrder.getTaskStartTime() : null, 
+                Objects.nonNull(waybillOrder) ? waybillOrder.getCreateTime() : null
+        );
+        taskInfo.setAcceptTime(formatDate(acceptTime));
+        log.debug("设置接单时间: {}", formatDate(acceptTime));
+
+        // 3. 设置完成时间:优先使用任务结束时间,若为空则尝试从子任务中获取卸货时间
+        Date finishTime = firstNotNull(
+                Objects.nonNull(waybillOrder) ? waybillOrder.getTaskEndTime() : null, 
+                Objects.nonNull(subtask) ? subtask.getUnloadTime() : null
+        );
+        taskInfo.setFinishTime(formatDate(finishTime));
+        log.debug("设置完成时间: {}", formatDate(finishTime));
+
+        // 4. 组装司机信息(姓名、电话、脱敏身份证号)
+        taskInfo.setDriverInfo(buildDriverInfo(waybillOrder));
+
+        // 5. 组装货物及重量信息(名称、规格、单位、各类重量数据)
+        taskInfo.setGoodsInfo(buildGoodsInfo(logisticsOrder, subtask, loadTicket, goods));
+
+        // 6. 组装车辆信息(车牌号、轴数-含缓存逻辑)
+        taskInfo.setTruckInfo(buildTruckInfo(waybillOrder, truckAxleCache));
+
+        // 7. 设置目的地城市名称
+        String destination = Objects.nonNull(unloadAddress) ? unloadAddress.getCityName() : null;
+        taskInfo.setDestination(destination);
+        log.debug("设置目的地: {}", destination);
+
+        // 8. 组装打印信息(主要基于装货单上传时间)
+        taskInfo.setPrintInfo(buildPrintInfo(loadTicket));
+
+        log.debug("单车任务信息组装完成,waybillNo:{}", Objects.nonNull(waybillOrder) ? waybillOrder.getWOrderNo() : null);
+        return taskInfo;
+    }
+
+    /**
+     * 组装司机信息。
+     */
+    /**
+     * 组装司机信息。
+     * <p>
+     * 从运单对象中提取司机姓名、电话及身份证号,并对身份证号进行脱敏处理以保护隐私。
+     * </p>
+     *
+     * @param waybillOrder 运单信息,包含司机相关字段
+     * @return 组装后的司机信息对象
+     */
+    private TradeOrderTransportInfoResp.DriverInfo buildDriverInfo(KwtWaybillOrder waybillOrder) {
+        log.debug("开始组装司机信息,waybillNo:{}", Objects.nonNull(waybillOrder) ? waybillOrder.getWOrderNo() : null);
+        
+        TradeOrderTransportInfoResp.DriverInfo driverInfo = new TradeOrderTransportInfoResp.DriverInfo();
+        
+        if (Objects.nonNull(waybillOrder)) {
+            // 设置司机姓名
+            driverInfo.setName(waybillOrder.getDriverName());
+            // 设置司机电话
+            driverInfo.setPhone(waybillOrder.getDriverPhone());
+            // 设置脱敏后的身份证号
+            String maskedIdCard = maskIdCard(waybillOrder.getDriverIdcard());
+            driverInfo.setIdCard(maskedIdCard);
+            
+            log.debug("司机信息组装完成,name:{}, phone:{}, idCardMasked:{}", 
+                    waybillOrder.getDriverName(), 
+                    waybillOrder.getDriverPhone(), 
+                    maskedIdCard);
+        } else {
+            log.warn("运单对象为空,无法组装司机信息");
+        }
+        
+        return driverInfo;
+    }
+
+    /**
+     * 组装货物及重量信息。
+     */
+    /**
+     * 组装货物及重量信息。
+     * <p>
+     * 该方法负责从物流订单、运单子任务、装货单及货物明细中提取并整合货物相关信息。
+     * 主要包含货物名称、规格、单位、委托量以及皮重、毛重、净重等关键指标。
+     * </p>
+     * <p>
+     * 数据取值优先级策略:
+     * 1. 基础属性(名称、规格):优先取自货物明细表 (KwtLogisticsOrderGoods)。
+     * 2. 单位与委托量:优先取自运单子任务 (KwtWaybillOrderSubtask),若缺失则降级使用物流订单 (KwtLogisticsOrder) 中的数据。
+     * 3. 重量信息:
+     *    - 皮重/毛重:直接取自装货单 (KwtWaybillOrderTicket)。
+     *    - 净重:采用多级兜底策略,优先级依次为:
+     *      a. 装货单记录的净重 (loadTicket.getAmount())
+     *      b. 计算得出的净重 (毛重 - 皮重)
+     *      c. 子任务记录的装载量 (subtask.getLoadAmount())
+     * </p>
+     *
+     * @param logisticsOrder 物流订单信息,用于获取基础单位和重量兜底
+     * @param subtask        运单子任务信息,包含实际执行层面的单位、委托量及装载量
+     * @param loadTicket     装货单信息,包含称重相关的皮重、毛重及净重数据
+     * @param goods          货物明细信息,包含货物名称、规格等静态属性
+     * @return 组装后的货物信息对象
+     */
+    private TradeOrderTransportInfoResp.GoodsInfo buildGoodsInfo(KwtLogisticsOrder logisticsOrder,
+                                                                 KwtWaybillOrderSubtask subtask,
+                                                                 KwtWaybillOrderTicket loadTicket,
+                                                                 KwtLogisticsOrderGoods goods) {
+        log.debug("开始组装货物及重量信息,logisticsOrderId:{}, waybillSubtaskId:{}, loadTicketId:{}, goodsId:{}", 
+                Objects.nonNull(logisticsOrder) ? logisticsOrder.getId() : null,
+                Objects.nonNull(subtask) ? subtask.getId() : null,
+                Objects.nonNull(loadTicket) ? loadTicket.getId() : null,
+                Objects.nonNull(goods) ? goods.getId() : null);
+
+        TradeOrderTransportInfoResp.GoodsInfo goodsInfo = new TradeOrderTransportInfoResp.GoodsInfo();
+        
+        // 1. 设置货物基础属性:名称和规格
+        if (Objects.nonNull(goods)) {
+            goodsInfo.setMaterialName(goods.getGoodsName());
+            goodsInfo.setSpecification(goods.getRemark());
+            log.debug("设置货物名称: {}, 规格: {}", goods.getGoodsName(), goods.getRemark());
+        } else {
+            log.warn("货物明细为空,无法设置货物名称和规格");
+        }
+
+        // 2. 设置计量单位:优先使用子任务中的单位,若无则使用物流订单中的单位
+        String unit = firstNotBlank(
+                Objects.nonNull(subtask) ? subtask.getUnit() : null, 
+                Objects.nonNull(logisticsOrder) ? logisticsOrder.getUnit() : null
+        );
+        goodsInfo.setUnit(unit);
+        log.debug("设置货物单位: {}", unit);
+
+        // 3. 设置委托任务量:优先使用子任务中的委托量,若无则使用物流订单中的总量
+        BigDecimal taskAmount = firstNotNull(
+                Objects.nonNull(subtask) ? subtask.getEntrustAmount() : null, 
+                Objects.nonNull(logisticsOrder) ? logisticsOrder.getAmount() : null
+        );
+        goodsInfo.setTaskAmount(taskAmount);
+        log.debug("设置委托任务量: {}", taskAmount);
+
+        // 4. 设置皮重和毛重:直接从装货单获取
+        if (Objects.nonNull(loadTicket)) {
+            goodsInfo.setTareWeight(loadTicket.getTareAmount());
+            goodsInfo.setGrossWeight(loadTicket.getGrossAmount());
+            log.debug("设置皮重: {}, 毛重: {}", loadTicket.getTareAmount(), loadTicket.getGrossAmount());
+        } else {
+            log.warn("装货单信息为空,无法设置皮重和毛重");
+        }
+
+        // 5. 计算并设置净重:采用多级兜底策略
+        // 优先级:装货单净重 > (毛重 - 皮重) > 子任务装载量
+        BigDecimal netWeight = firstNotNull(
+                Objects.nonNull(loadTicket) ? loadTicket.getAmount() : null,
+                subtract(goodsInfo.getGrossWeight(), goodsInfo.getTareWeight()),
+                Objects.nonNull(subtask) ? subtask.getLoadAmount() : null
+        );
+        goodsInfo.setNetWeight(netWeight);
+        log.debug("设置净重: {} (来源优先级: 装货单记录 > 计算值 > 子任务记录)", netWeight);
+
+        log.debug("货物及重量信息组装完成,materialName:{}, netWeight:{}", 
+                goodsInfo.getMaterialName(), goodsInfo.getNetWeight());
+        return goodsInfo;
+    }
+
+    /**
+     * 组装车辆信息。
+     */
+    /**
+     * 组装车辆信息。
+     * <p>
+     * 该方法负责从运单对象中提取车辆相关信息,并查询车辆轴数。
+     * 为了减少远程 Feign 调用的次数,使用了传入的缓存 Map (truckAxleCache) 来存储已查询过的车牌号对应的轴数。
+     * </p>
+     *
+     * @param waybillOrder   运单信息,包含车牌号等基础数据
+     * @param truckAxleCache 车辆轴数缓存 Map,Key 为车牌号,Value 为轴数
+     * @return 组装后的车辆信息对象
+     */
+    private TradeOrderTransportInfoResp.TruckInfo buildTruckInfo(KwtWaybillOrder waybillOrder, Map<String, String> truckAxleCache) {
+        log.debug("开始组装车辆信息,waybillNo:{}", Objects.nonNull(waybillOrder) ? waybillOrder.getWOrderNo() : null);
+        
+        TradeOrderTransportInfoResp.TruckInfo truckInfo = new TradeOrderTransportInfoResp.TruckInfo();
+        
+        if (Objects.nonNull(waybillOrder)) {
+            // 设置车牌号
+            String truckNo = waybillOrder.getTruckNo();
+            truckInfo.setTruckNo(truckNo);
+            log.debug("设置车牌号: {}", truckNo);
+
+            // 获取车辆轴数:优先从缓存中获取,若不存在则调用 queryTruckAxle 查询并放入缓存
+            // 使用 StringUtils.defaultString 防止 key 为 null 导致 Map 操作异常
+            String cacheKey = StringUtils.defaultString(truckNo);
+            String truckAxle = truckAxleCache.computeIfAbsent(cacheKey, this::queryTruckAxle);
+            truckInfo.setTruckAxle(truckAxle);
+            log.debug("设置车辆轴数: {} (来源: {})", truckAxle, truckAxleCache.containsKey(cacheKey) && truckAxle != null ? "缓存/新查询" : "查询失败/null");
+        } else {
+            log.warn("运单对象为空,无法组装车辆信息");
+        }
+        
+        log.debug("车辆信息组装完成,truckNo:{}, truckAxle:{}", 
+                Objects.nonNull(truckInfo.getTruckNo()) ? truckInfo.getTruckNo() : "N/A", 
+                truckInfo.getTruckAxle());
+        return truckInfo;
+    }
+
+    /**
+     * 通过车辆 Feign 查询车辆轴数。
+     */
+    /**
+     * 通过车辆 Feign 查询车辆轴数。
+     * <p>
+     * 该方法调用远程车队服务接口,根据车牌号获取车辆的轴数信息。
+     * 主要用于补充运输任务中的车辆详细属性,以支持后续的运费计算或合规性校验。
+     * </p>
+     *
+     * @param truckNo 车牌号
+     * @return 车辆轴数(字符串格式),若查询失败、无数据或发生异常则返回 null
+     */
+    private String queryTruckAxle(String truckNo) {
+        // 1. 参数校验:车牌号为空直接返回
+        if (StringUtils.isBlank(truckNo)) {
+            log.debug("查询车辆轴数跳过:车牌号为空");
+            return null;
+        }
+        
+        log.debug("开始查询车辆轴数,truckNo:{}", truckNo);
+        try {
+            // 2. 调用远程 Feign 接口
+            HttpResult result = fleetTruckFeignService.findByTruckNo(truckNo);
+            
+            // 3. 校验响应结果有效性
+            // - 响应对象非空
+            // - 状态码为成功 (HttpStatus.SUCCESS_CODE)
+            // - 返回数据结构为 Map 类型
+            if (Objects.isNull(result) || result.getCode() != HttpStatus.SUCCESS_CODE || !(result.getData() instanceof Map<?, ?> dataMap)) {
+                log.warn("查询车辆轴数失败:远程服务返回异常或数据格式不符,truckNo:{}, resultCode:{}, dataType:{}", 
+                        truckNo, 
+                        Objects.nonNull(result) ? result.getCode() : "N/A", 
+                        Objects.nonNull(result) && Objects.nonNull(result.getData()) ? result.getData().getClass().getSimpleName() : "N/A");
+                return null;
+            }
+
+            // 4. 提取轴数字段 "carAxis"
+            Object carAxis = dataMap.get("carAxis");
+            
+            // 5. 转换并返回结果
+            if (Objects.nonNull(carAxis)) {
+                String axleStr = String.valueOf(carAxis);
+                log.debug("成功查询到车辆轴数,truckNo:{}, axle:{}", truckNo, axleStr);
+                return axleStr;
+            } else {
+                log.debug("远程服务返回数据中未包含轴数信息,truckNo:{}", truckNo);
+                return null;
+            }
+            
+        } catch (Exception e) {
+            // 6. 异常处理:记录错误日志并返回 null,避免影响主流程
+            log.error("Feign 调用查询车辆轴数发生异常,truckNo:{}, errorMessage:{}", truckNo, e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 组装打印信息,当前运输库无独立打印记录时使用装货单上传时间兜底。
+     */
+    /**
+     * 组装打印信息。
+     * <p>
+     * 该方法负责构建任务相关的打印信息对象。
+     * 由于当前运输库中可能没有独立的打印记录表,因此采用“装货单上传时间”作为打印时间的兜底数据。
+     * </p>
+     *
+     * @param loadTicket 装货单信息,包含上传时间等关键字段
+     * @return 组装后的打印信息对象,若装货单为空则返回仅包含空字段的对象
+     */
+    private TradeOrderTransportInfoResp.PrintInfo buildPrintInfo(KwtWaybillOrderTicket loadTicket) {
+        log.debug("开始组装打印信息,loadTicketId:{}", Objects.nonNull(loadTicket) ? loadTicket.getId() : null);
+        
+        TradeOrderTransportInfoResp.PrintInfo printInfo = new TradeOrderTransportInfoResp.PrintInfo();
+        
+        if (Objects.nonNull(loadTicket)) {
+            // 使用装货单的上传时间作为打印时间
+            Date uploadingTime = loadTicket.getUploadingTime();
+            String printTimeStr = formatDate(uploadingTime);
+            printInfo.setPrintTime(printTimeStr);
+            
+            log.debug("设置打印时间(源自装货单上传时间): {}", printTimeStr);
+        } else {
+            log.warn("装货单信息为空,无法设置打印时间");
+        }
+        
+        log.debug("打印信息组装完成");
+        return printInfo;
+    }
+
+    /**
+     * 选择最新的运单子任务。
+     * <p>
+     * 当存在多个子任务时,根据更新时间(UpdateTime)进行比较,保留最新更新的记录。
+     * 若更新时间为空,则视为较小值(排在后面),确保非空时间优先被选中。
+     * </p>
+     *
+     * @param source 源子任务
+     * @param target 目标子任务
+     * @return 更新时间较晚的子任务;若两者均为 null 或更新时间相同,返回 source
+     */
+    private KwtWaybillOrderSubtask chooseLatestSubtask(KwtWaybillOrderSubtask source, KwtWaybillOrderSubtask target) {
+        log.debug("比较运单子任务更新时间,sourceId:{}, sourceUpdateTime:{}, targetId:{}, targetUpdateTime:{}", 
+                Objects.nonNull(source) ? source.getId() : null,
+                Objects.nonNull(source) ? source.getUpdateTime() : null,
+                Objects.nonNull(target) ? target.getId() : null,
+                Objects.nonNull(target) ? target.getUpdateTime() : null);
+
+        KwtWaybillOrderSubtask latest = Comparator.nullsLast(Comparator.comparing(KwtWaybillOrderSubtask::getUpdateTime, Comparator.nullsLast(Date::compareTo)))
+                .compare(source, target) >= 0 ? source : target;
+        
+        log.debug("选择最新的运单子任务,selectedId:{}, selectedUpdateTime:{}", 
+                Objects.nonNull(latest) ? latest.getId() : null,
+                Objects.nonNull(latest) ? latest.getUpdateTime() : null);
+                
+        return latest;
+    }
+
+    /**
+     * 选择最新的装货单。
+     * <p>
+     * 当存在多个装货单时,根据更新时间(UpdateTime)进行比较,保留最新更新的记录。
+     * 若更新时间为空,则视为较小值(排在后面),确保非空时间优先被选中。
+     * </p>
+     *
+     * @param source 源装货单
+     * @param target 目标装货单
+     * @return 更新时间较晚的装货单;若两者均为 null 或更新时间相同,返回 source
+     */
+    private KwtWaybillOrderTicket chooseLatestTicket(KwtWaybillOrderTicket source, KwtWaybillOrderTicket target) {
+        log.debug("比较装货单更新时间,sourceId:{}, sourceUpdateTime:{}, targetId:{}, targetUpdateTime:{}", 
+                Objects.nonNull(source) ? source.getId() : null,
+                Objects.nonNull(source) ? source.getUpdateTime() : null,
+                Objects.nonNull(target) ? target.getId() : null,
+                Objects.nonNull(target) ? target.getUpdateTime() : null);
+
+        KwtWaybillOrderTicket latest = Comparator.nullsLast(Comparator.comparing(KwtWaybillOrderTicket::getUpdateTime, Comparator.nullsLast(Date::compareTo)))
+                .compare(source, target) >= 0 ? source : target;
+        
+        log.debug("选择最新的装货单,selectedId:{}, selectedUpdateTime:{}", 
+                Objects.nonNull(latest) ? latest.getId() : null,
+                Objects.nonNull(latest) ? latest.getUpdateTime() : null);
+                
+        return latest;
+    }
+
+    public static String maskIdCard(String idCard) {
+        if (StringUtils.isBlank(idCard) || idCard.length() < 8) {
+            return idCard;
+        }
+        return idCard.substring(0, 5) + "****" + idCard.substring(idCard.length() - 4);
+    }
+
+    @SafeVarargs
+    public static <T> T firstNotNull(T... values) {
+        if (Objects.isNull(values)) {
+            return null;
+        }
+        for (T value : values) {
+            if (Objects.nonNull(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    public static BigDecimal subtract(BigDecimal source, BigDecimal target) {
+        if (Objects.isNull(source) || Objects.isNull(target)) {
+            return null;
+        }
+        return source.subtract(target);
+    }
+
+    private String firstNotBlank(String source, String target) {
+        return StringUtils.isNotBlank(source) ? source : target;
+    }
+
+    private String formatDate(Date date) {
+        if (Objects.isNull(date)) {
+            return null;
+        }
+        return new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
+    }
+}

+ 54 - 0
sckw-modules/sckw-transport/src/test/java/com/sckw/transport/service/TradeOrderTransportInfoServiceTest.java

@@ -0,0 +1,54 @@
+package com.sckw.transport.service;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+
+/**
+ * 贸易订单运输信息查询服务单元测试。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+public class TradeOrderTransportInfoServiceTest {
+
+    /**
+     * 身份证号脱敏应保留前五位和后四位。
+     */
+    @Test
+    public void maskIdCardShouldKeepPrefixAndSuffix() {
+        String actual = TradeOrderTransportInfoService.maskIdCard("519001199001011718");
+
+        Assert.assertEquals("51900****1718", actual);
+    }
+
+    /**
+     * 身份证号为空或长度不足时直接返回原值。
+     */
+    @Test
+    public void maskIdCardShouldReturnOriginalWhenInvalid() {
+        Assert.assertNull(TradeOrderTransportInfoService.maskIdCard(null));
+        Assert.assertEquals("1234567", TradeOrderTransportInfoService.maskIdCard("1234567"));
+    }
+
+    /**
+     * firstNotNull 应返回第一个非空值。
+     */
+    @Test
+    public void firstNotNullShouldReturnFirstAvailableValue() {
+        String actual = TradeOrderTransportInfoService.firstNotNull(null, "A", "B");
+
+        Assert.assertEquals("A", actual);
+    }
+
+    /**
+     * 净重计算应使用毛重减皮重。
+     */
+    @Test
+    public void subtractShouldCalculateNetWeight() {
+        BigDecimal actual = TradeOrderTransportInfoService.subtract(new BigDecimal("36.83"), new BigDecimal("12.54"));
+
+        Assert.assertEquals(new BigDecimal("24.29"), actual);
+    }
+}