Ver Fonte

Merge remote-tracking branch 'origin/dev_20260131' into dev_20260131_youshen430

# Conflicts:
#	sckw-modules/sckw-order/src/main/java/com/sckw/order/serivce/KwoTradeOrderService.java
#	sckw-modules/sckw-order/src/main/java/com/sckw/order/task/TradeOrderTask.java
#	sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsUserService.java
#	sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/KwtWaybillOrderController.java
donglang há 1 mês atrás
pai
commit
978297dbc1
15 ficheiros alterados com 1583 adições e 119 exclusões
  1. 7 0
      sckw-auth/src/main/java/com/sckw/auth/service/impl/AuthServiceImpl.java
  2. 25 0
      sckw-modules-api/sckw-fleet-api/src/main/java/com/sckw/fleet/api/feign/FleetTruckFeignService.java
  3. 25 3
      sckw-modules/sckw-order/src/main/java/com/sckw/order/serivce/KwoTradeOrderService.java
  4. 19 41
      sckw-modules/sckw-order/src/main/java/com/sckw/order/task/TradeOrderTask.java
  5. 3 5
      sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsUserService.java
  6. 6 0
      sckw-modules/sckw-transport/pom.xml
  7. 52 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/KwtWaybillOrderController.java
  8. 29 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/model/param/TradeOrderTransportQueryReq.java
  9. 29 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/model/param/WaybillTransportQueryReq.java
  10. 160 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/model/vo/TradeOrderTransportInfoResp.java
  11. 14 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/repository/KwtLogisticsOrderRepository.java
  12. 173 38
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/KwtLogisticsConsignmentService.java
  13. 885 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/TradeOrderTransportInfoService.java
  14. 102 32
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/app/WaybillOrderService.java
  15. 54 0
      sckw-modules/sckw-transport/src/test/java/com/sckw/transport/service/TradeOrderTransportInfoServiceTest.java

+ 7 - 0
sckw-auth/src/main/java/com/sckw/auth/service/impl/AuthServiceImpl.java

@@ -171,6 +171,13 @@ public class AuthServiceImpl implements IAuthService {
         List<LoginResVo1.TabBarItem> tabBar = buildAppTabBar(loginBase, null,1,loginRes.getEntTypes());
         if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(tabBar)) {
             loginRes.setTabBar(tabBar);
+            if (org.apache.commons.lang3.StringUtils.equals(loginRes.getEntTypes(), "1")  ) {
+                loginRes.setRoleName("供应商管理员");
+            } else if (org.apache.commons.lang3.StringUtils.equals(loginRes.getEntTypes(), "2")) {
+                 loginRes.setRoleName("采购商管理员");
+            }else if (org.apache.commons.lang3.StringUtils.equals(loginRes.getEntTypes(), "3")) {
+                 loginRes.setRoleName("物流商管理员");
+            }
         }
         applyAppModulePermissions(loginRes, loginBase, null);
         loginRes.setRefreshToken(refreshToken);

+ 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);
+}

+ 25 - 3
sckw-modules/sckw-order/src/main/java/com/sckw/order/serivce/KwoTradeOrderService.java

@@ -43,6 +43,7 @@ import com.sckw.manage.api.RemoteManageService;
 import com.sckw.manage.api.model.dto.res.FindEntCooperateResVo;
 import com.sckw.mongo.model.SckwTradeOrder;
 import com.sckw.order.api.model.*;
+import com.sckw.order.dao.KwoTradeOrderGoodsMapper;
 import com.sckw.order.dao.KwoTradeOrderArchiveMapper;
 import com.sckw.order.dao.KwoTradeOrderMapper;
 import com.sckw.order.enums.ContractStatusEnum;
@@ -168,6 +169,8 @@ public class KwoTradeOrderService {
     private final KwoTradeOrderTransportService kwoTradeOrderTransportService;
     private final KwoTradeOrderUnitRepository kwoTradeOrderUnitRepository;
     @Resource
+    private KwoTradeOrderGoodsMapper kwoTradeOrderGoodsMapper;
+    @Resource
     private DataPermissionFeignService dataPermissionFeignService;
     @Value("${url.order.list.valet.pc}")
     private String pcValetListUrl;
@@ -2955,11 +2958,21 @@ public class KwoTradeOrderService {
             unFreezePrepaidDto.setTradeType(5);
             unFreezePrepaidDto.setRemark("贸易订单:" + unFreezePrepaidDto.getOrderNo() + ",");
             log.info("贸易订单号:{},订单完结调用钱包服务解冻预付余额,参数:{}", unFreezePrepaidDto.getOrderNo(), JSON.toJSONString(unFreezePrepaidDto));
-            BaseResult<Object> unFreezeResult = paymentFeignService.updatePrepaidBalance(unFreezePrepaidDto);
+            BaseResult<Object> unFreezeResult = new BaseResult<>();
+            try {
+                unFreezeResult = paymentFeignService.updatePrepaidBalance(unFreezePrepaidDto);
+            } catch (Exception e) {
+                log.error("订单完结调用钱包服务解冻预付余额异常 {}", e.getMessage());
+            }
             if (unFreezeResult.getCode() != HttpStatus.SUCCESS_CODE) {
                 log.error("贸易订单号:{},订单完结解冻失败,异常信息为:{}", unFreezePrepaidDto.getOrderNo(), unFreezeResult.getMessage());
                 throw new BusinessException("贸易订单号:" + unFreezePrepaidDto.getOrderNo() + ",订单完结解冻失败");
             }
+            KwoTradeOrderGoods kwoTradeOrderGoods = kwoTradeOrderGoodsMapper.selectOne(new LambdaQueryWrapper<KwoTradeOrderGoods>()
+                    .eq(KwoTradeOrderGoods::getDelFlag, 0)
+                    .eq(KwoTradeOrderGoods::getTOrderId, kwoTradeOrder.getId())
+                    .last("limit 1")
+            );
             //2计算订单金额
             WalletPrepaidDto consumePrepaidDto = new WalletPrepaidDto();
             consumePrepaidDto.setOrderNo(kwoTradeOrder.getTOrderNo());
@@ -2967,10 +2980,19 @@ public class KwoTradeOrderService {
             consumePrepaidDto.setSupEntId(unitMap.get(String.valueOf(2)).getEntId());
             consumePrepaidDto.setOrderType(4);
             consumePrepaidDto.setTradeType(6);
-            consumePrepaidDto.setTradeAmount(kwoTradeOrder.getPrice());
+            if (Objects.equals(kwoTradeOrder.getChargeType(), 1)) {
+                consumePrepaidDto.setTradeAmount(NumberUtil.mul(kwoTradeOrder.getLoadAmount(), kwoTradeOrderGoods.getUnitPrice()));
+            } else {
+                consumePrepaidDto.setTradeAmount(NumberUtil.mul(kwoTradeOrder.getUnloadAmount(), kwoTradeOrderGoods.getUnitPrice()));
+            }
             consumePrepaidDto.setRemark("贸易订单号:" + consumePrepaidDto.getOrderNo() + ",订单完结消费");
             log.info("贸易订单号:{},订单完结调用钱包服务消费预付余额,参数:{}", consumePrepaidDto.getOrderNo(), JSON.toJSONString(consumePrepaidDto));
-            BaseResult<Object> consumeResult = paymentFeignService.updatePrepaidBalance(consumePrepaidDto);
+            BaseResult<Object> consumeResult = new BaseResult<>();
+            try {
+                consumeResult = paymentFeignService.updatePrepaidBalance(consumePrepaidDto);
+            } catch (Exception e) {
+                log.error("订单完结调用钱包服务消费预付余额异常 {}", e.getMessage());
+            }
             if (consumeResult.getCode() != HttpStatus.SUCCESS_CODE) {
                 log.error("贸易订单号:{},订单完结消费失败,异常信息为:{}", consumePrepaidDto.getOrderNo(), consumeResult.getMessage());
                 throw new BusinessException("贸易订单号:" + consumePrepaidDto.getOrderNo() + ",订单完结消费失败");

+ 19 - 41
sckw-modules/sckw-order/src/main/java/com/sckw/order/task/TradeOrderTask.java

@@ -158,7 +158,7 @@ public class TradeOrderTask {
                                     log.error("物流订单结算失败 {}", e.getMessage());
                                 }
                                 kwoTradeOrder.setPrice(NumberUtil.mul(sum, kwoTradeOrderGoods.getUnitPrice()));
-                                calculatePrepaidBalance(kwoTradeOrder, unitMap);
+                                calculatePrepaidBalance(kwoTradeOrder, unitMap, kwoTradeOrderGoods.getUnitPrice());
                             } catch (Exception ex) {
                                 log.error("更新订单异常{}", ex.getMessage());
                             }
@@ -214,43 +214,7 @@ public class TradeOrderTask {
         }
     }
 
-    @Scheduled(cron = "0 0/10 * * * ?")
-    public void task2() {
-        List<TradeOrderDto> tradeOrderDtos = kwoTradeOrderMapper.joinSelect();
-        if (CollUtil.isNotEmpty(tradeOrderDtos)) {
-            Map<String, Map<String, String>> dict = remoteSystemService.queryDictByType(List.of(DictTypeEnum.GOODS_SPEC.getType()));
-            Map<String, String> goodsSpecMap = dict.get(DictTypeEnum.GOODS_SPEC.getType());
-
-            Map<Long, KwpGoods> goodsByIds = goodsInfoService.getGoodsByIds(tradeOrderDtos.stream().map(TradeOrderDto::getGoodsId).toList());
-            List<KwoTradeOrderArchive> list = tradeOrderDtos.stream().map(d -> {
-                KwoTradeOrderArchive kwoTradeOrderArchive = new KwoTradeOrderArchive();
-                kwoTradeOrderArchive.setTradeOrderId(d.getId());
-                kwoTradeOrderArchive.setGoodsId(d.getGoodsId());
-                KwpGoods kwpGoods = goodsByIds.get(d.getGoodsId());
-                if (Objects.nonNull(kwpGoods)) {
-                    DictProduct dictProduct = remoteSystemService.queryProductName(kwpGoods.getGoodsType());
-                    if (Objects.nonNull(dictProduct)) {
-                        kwoTradeOrderArchive.setProductName(dictProduct.getLabel());
-                    }
-                    kwoTradeOrderArchive.setProductSpec(goodsSpecMap.get(kwpGoods.getSpec()));
-                }
-
-                kwoTradeOrderArchive.setUnitPrice(d.getUnitPrice());
-                kwoTradeOrderArchive.setAmount(d.getAmount());
-                return kwoTradeOrderArchive;
-
-            }).toList();
-            kwoTradeOrderArchiveService.saveBatch(list);
-            for (TradeOrderDto tradeOrderDto : tradeOrderDtos) {
-                KwoTradeOrder kwoTradeOrder = new KwoTradeOrder();
-                kwoTradeOrder.setId(tradeOrderDto.getId());
-                kwoTradeOrder.setArchiveFlag(1);
-                kwoTradeOrderMapper.updateById(kwoTradeOrder);
-            }
-        }
-    }
-
-    private void calculatePrepaidBalance(KwoTradeOrder kwoTradeOrder, Map<String, KwoTradeOrderUnit> unitMap) {
+    private void calculatePrepaidBalance(KwoTradeOrder kwoTradeOrder, Map<String, KwoTradeOrderUnit> unitMap, BigDecimal unitPrice) {
         if (Objects.equals(kwoTradeOrder.getStatus(), TradeOrderStatusEnum.SUCCESS.getCode())) {
             //贸易订单已完结,计算预付余额
             if (Objects.equals(kwoTradeOrder.getSettlement(), 1)) {
@@ -271,7 +235,12 @@ public class TradeOrderTask {
                 unFreezePrepaidDto.setTradeType(5);
                 unFreezePrepaidDto.setRemark("贸易订单:" + unFreezePrepaidDto.getOrderNo() + ",");
                 log.info("贸易订单号:{},订单完结调用钱包服务解冻预付余额,参数:{}", unFreezePrepaidDto.getOrderNo(), JSON.toJSONString(unFreezePrepaidDto));
-                BaseResult<Object> unFreezeResult = paymentFeignService.updatePrepaidBalance(unFreezePrepaidDto);
+                BaseResult<Object> unFreezeResult = new BaseResult<>();
+                try {
+                    unFreezeResult = paymentFeignService.updatePrepaidBalance(unFreezePrepaidDto);
+                } catch (Exception e) {
+                    log.error("订单完结调用钱包服务解冻预付余额异常:{}", e.getMessage());
+                }
                 if (unFreezeResult.getCode() != HttpStatus.SUCCESS_CODE) {
                     log.error("贸易订单号:{},订单完结解冻失败,异常信息为:{}", unFreezePrepaidDto.getOrderNo(), unFreezeResult.getMessage());
                     throw new BusinessException("贸易订单号:" + unFreezePrepaidDto.getOrderNo() + ",订单完结解冻失败");
@@ -283,10 +252,19 @@ public class TradeOrderTask {
                 consumePrepaidDto.setSupEntId(unitMap.get(String.valueOf(2)).getEntId());
                 consumePrepaidDto.setOrderType(4);
                 consumePrepaidDto.setTradeType(6);
-                consumePrepaidDto.setTradeAmount(kwoTradeOrder.getPrice());
+                if (Objects.equals(kwoTradeOrder.getChargeType(), 1)) {
+                    consumePrepaidDto.setTradeAmount(NumberUtil.mul(kwoTradeOrder.getLoadAmount(), unitPrice));
+                } else {
+                    consumePrepaidDto.setTradeAmount(NumberUtil.mul(kwoTradeOrder.getUnloadAmount(), unitPrice));
+                }
                 consumePrepaidDto.setRemark("贸易订单号:" + consumePrepaidDto.getOrderNo() + ",订单完结消费");
                 log.info("贸易订单号:{},订单完结调用钱包服务消费预付余额,参数:{}", consumePrepaidDto.getOrderNo(), JSON.toJSONString(consumePrepaidDto));
-                BaseResult<Object> consumeResult = paymentFeignService.updatePrepaidBalance(consumePrepaidDto);
+                BaseResult<Object> consumeResult = new BaseResult<>();
+                try {
+                    consumeResult = paymentFeignService.updatePrepaidBalance(consumePrepaidDto);
+                } catch (Exception e) {
+                    log.error("订单完结调用钱包服务消费预付余额异常:{}", e.getMessage());
+                }
                 if (consumeResult.getCode() != HttpStatus.SUCCESS_CODE) {
                     log.error("贸易订单号:{},订单完结消费失败,异常信息为:{}", consumePrepaidDto.getOrderNo(), consumeResult.getMessage());
                     throw new BusinessException("贸易订单号:" + consumePrepaidDto.getOrderNo() + ",订单完结消费失败");

+ 3 - 5
sckw-modules/sckw-system/src/main/java/com/sckw/system/service/KwsUserService.java

@@ -245,14 +245,12 @@ public class KwsUserService {
             kwsMenus = kwsMenus.stream().filter(item -> item.getClientType().equals(Global.NUMERICAL_TWO)).collect(Collectors.toList());
 
             //企业属性 (1供应商,2采购商,34PL物流,43PL物流)
-//            if (org.apache.commons.lang3.StringUtils.equals("1", reqDto.getEntTypes())) {
+//            if (org.apache.commons.lang3.StringUtils.equals("1",reqDto.getEntTypes())){
 //                editRoleReqVo.setName("供应商管理员");
-//            } else if (org.apache.commons.lang3.StringUtils.equals("2", reqDto.getEntTypes())) {
+//            }else if (org.apache.commons.lang3.StringUtils.equals("2",reqDto.getEntTypes())){
 //                editRoleReqVo.setName("采购商管理员");
-//            } else if (org.apache.commons.lang3.StringUtils.equals("3", reqDto.getEntTypes())) {
+//            }else if (org.apache.commons.lang3.StringUtils.equals("3",reqDto.getEntTypes())){
 //                editRoleReqVo.setName("物流商管理员");
-//            } else {
-//                editRoleReqVo.setName(Global.MANAGE_NAME);
 //            }
             editRoleReqVo.setName(Global.MANAGE_NAME);
             editRoleReqVo.setCurrentUnitId(null);

+ 6 - 0
sckw-modules/sckw-transport/pom.xml

@@ -158,6 +158,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>com.sckw</groupId>
             <artifactId>sckw-payment-api</artifactId>

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

@@ -21,12 +21,16 @@ import com.sckw.payment.api.model.dto.common.R;
 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;
 import com.sckw.transport.model.vo.*;
 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;
@@ -59,6 +63,8 @@ public class KwtWaybillOrderController {
     KwtWaybillOrderV1Service waybillOrderV1Service;
     @Autowired
     TransportServiceImpl transportService;
+    @Autowired
+    TradeOrderTransportInfoService tradeOrderTransportInfoService;
 
     /**
      * @param params 请求参数
@@ -761,4 +767,50 @@ public class KwtWaybillOrderController {
     public R<WaybillOrderSaleVo> saleSum() {
         return R.ok(waybillOrderV1Service.saleSum());
     }
+
+
+
+    /**
+     * 根据贸易订单号查询运输相关信息。
+     *
+     * @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));
+    }
 }

+ 173 - 38
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/KwtLogisticsConsignmentService.java

@@ -2452,13 +2452,14 @@ public class KwtLogisticsConsignmentService {
         Map<String, KwtLogisticsOrderAddress> addressMap = addressMapFuture.join();
         Map<Long, KwtLogisticsOrderContract> contractMap = contractMapFuture.join();
         Map<Long, List<KwtWaybillOrderSubtask>> subtaskMap = processSubtaskData(subtaskListFuture.join());
+        Map<Long, List<KwtWaybillOrderTicket>> ticketMap = processTicketData(subtaskListFuture.join());
         Map<Long, OrderDetailVo> tradeMap = tradeMapFuture.join();
         Map<Long, List<KwtLogisticsOrder>> tradeLogOrderMap = groupByTradeOrder(records);
         log.debug("查询结果处理完成");
 
         log.debug("相关订单数据获取完成");
         return new RelatedOrderData(unitMap, goodsData, addressMap, contractMap,
-                subtaskMap, tradeMap, tradeLogOrderMap);
+                subtaskMap, ticketMap, tradeMap, tradeLogOrderMap);
     }
 
     /**
@@ -2528,6 +2529,31 @@ public class KwtLogisticsConsignmentService {
                 .collect(Collectors.groupingBy(KwtWaybillOrderSubtask::getLOrderId));
     }
 
+    /**
+     * 处理磅单数据,构建运单ID与磅单列表映射
+     */
+    private Map<Long, List<KwtWaybillOrderTicket>> processTicketData(List<KwtWaybillOrderSubtask> subtaskList) {
+        if (CollectionUtils.isEmpty(subtaskList)) {
+            return Maps.newHashMap();
+        }
+        List<Long> wOrderIds = subtaskList.stream()
+                .filter(Objects::nonNull)
+                .map(KwtWaybillOrderSubtask::getWOrderId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(wOrderIds)) {
+            return Maps.newHashMap();
+        }
+        List<KwtWaybillOrderTicket> tickets = waybillOrderTicketRepository.queryByWOrderIds(wOrderIds);
+        if (CollectionUtils.isEmpty(tickets)) {
+            return Maps.newHashMap();
+        }
+        return tickets.stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(KwtWaybillOrderTicket::getWOrderId));
+    }
+
     /**
      * 按贸易订单分组
      */
@@ -2584,7 +2610,8 @@ public class KwtLogisticsConsignmentService {
                         dictMap,
                         relatedData.getTradeMap(),
                         relatedData.getTradeLogOrderMap(),
-                        relatedData.getSubtaskMap()))
+                        relatedData.getSubtaskMap(),
+                        relatedData.getTicketMap()))
                 .collect(Collectors.toList());
     }
 
@@ -2763,6 +2790,7 @@ public class KwtLogisticsConsignmentService {
         private final Map<String, KwtLogisticsOrderAddress> addressMap;
         private final Map<Long, KwtLogisticsOrderContract> contractMap;
         private final Map<Long, List<KwtWaybillOrderSubtask>> subtaskMap;
+        private final Map<Long, List<KwtWaybillOrderTicket>> ticketMap;
         private final Map<Long, OrderDetailVo> tradeMap;
         private final Map<Long, List<KwtLogisticsOrder>> tradeLogOrderMap;
 
@@ -2771,6 +2799,7 @@ public class KwtLogisticsConsignmentService {
                                 Map<String, KwtLogisticsOrderAddress> addressMap,
                                 Map<Long, KwtLogisticsOrderContract> contractMap,
                                 Map<Long, List<KwtWaybillOrderSubtask>> subtaskMap,
+                                Map<Long, List<KwtWaybillOrderTicket>> ticketMap,
                                 Map<Long, OrderDetailVo> tradeMap,
                                 Map<Long, List<KwtLogisticsOrder>> tradeLogOrderMap) {
             this.unitMap = unitMap;
@@ -2778,6 +2807,7 @@ public class KwtLogisticsConsignmentService {
             this.addressMap = addressMap;
             this.contractMap = contractMap;
             this.subtaskMap = subtaskMap;
+            this.ticketMap = ticketMap;
             this.tradeMap = tradeMap;
             this.tradeLogOrderMap = tradeLogOrderMap;
         }
@@ -2802,6 +2832,10 @@ public class KwtLogisticsConsignmentService {
             return subtaskMap;
         }
 
+        public Map<Long, List<KwtWaybillOrderTicket>> getTicketMap() {
+            return ticketMap;
+        }
+
         public Map<Long, OrderDetailVo> getTradeMap() {
             return tradeMap;
         }
@@ -3132,7 +3166,8 @@ public class KwtLogisticsConsignmentService {
                                                             Map<String, Map<String, String>> dictValueAndDictResDtoMap,
                                                             Map<Long, OrderDetailVo> tradeIdAndOrderDetailVoMap,
                                                             Map<Long, List<KwtLogisticsOrder>> tradeIdAndLogOrderList,
-                                                            Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList) {
+                                                            Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList,
+                                                            Map<Long, List<KwtWaybillOrderTicket>> waybillOrderIdAndTicketList) {
         LogisticsOrderResp logisticsOrderResp = new LogisticsOrderResp();
         logisticsOrderResp.setLogisticsOrderId(String.valueOf(kwtLogisticsOrder.getId()));
         logisticsOrderResp.setLogisticsOrderNo(kwtLogisticsOrder.getLOrderNo());
@@ -3169,15 +3204,50 @@ public class KwtLogisticsConsignmentService {
         BigDecimal expectedTransportPrice =
                 Objects.nonNull(kwtLogisticsOrder.getPrice()) && Objects.nonNull(orderDetailVo.getAmount()) ?
                         kwtLogisticsOrder.getPrice().multiply(orderDetailVo.getAmount()) : BigDecimal.ZERO;
-        BigDecimal actualTransportPrice = Objects.nonNull(kwtLogisticsOrder.getPrice()) && Objects.nonNull(kwtLogisticsOrder.getUnloadAmount()) ?
-                kwtLogisticsOrder.getPrice().multiply(kwtLogisticsOrder.getUnloadAmount()) : BigDecimal.ZERO;
+//        BigDecimal actualTransportPrice = Objects.nonNull(kwtLogisticsOrder.getPrice()) && Objects.nonNull(kwtLogisticsOrder.getUnloadAmount()) ?
+//                kwtLogisticsOrder.getPrice().multiply(kwtLogisticsOrder.getUnloadAmount()) : BigDecimal.ZERO;
 
         logisticsOrderResp.setExpectedTransportPrice(expectedTransportPrice.setScale(2, RoundingMode.HALF_UP).toPlainString() + priceUnit);
-        logisticsOrderResp.setActualTransportPrice(actualTransportPrice.setScale(2, RoundingMode.HALF_UP).toPlainString() + priceUnit);
-        logisticsOrderResp.setLoadAmount((Objects.nonNull(kwtLogisticsOrder.getLoadAmount()) ?
-                kwtLogisticsOrder.getLoadAmount().setScale(2, RoundingMode.HALF_UP).toPlainString() : "0.00") + kwtLogisticsOrder.getUnit());
-        logisticsOrderResp.setUnloadAmount((Objects.nonNull(kwtLogisticsOrder.getUnloadAmount()) ?
-                kwtLogisticsOrder.getUnloadAmount().setScale(2, RoundingMode.HALF_UP).toPlainString() : "0.00") + kwtLogisticsOrder.getUnit());
+
+        List<KwtWaybillOrderSubtask> subtasks = logisticsOrderIdAndSubtaskList.getOrDefault(kwtLogisticsOrder.getId(), new ArrayList<>());
+        BigDecimal transportPrice = BigDecimal.ZERO;
+        BigDecimal loadAmount= BigDecimal.ZERO;
+        BigDecimal unloadAmount = BigDecimal.ZERO;
+        //用于计算
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        if (org.apache.commons.lang3.StringUtils.equals(kwtLogisticsOrder.getBillingMode(),
+                DictEnum.CHARGING_TYPE_1.getValue())) {
+            loadAmount = subtasks.stream().filter(x->Objects.nonNull(x.getLoadAmount()))
+                    .map(KwtWaybillOrderSubtask::getLoadAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            transportPrice = Objects.nonNull(kwtLogisticsOrder.getPrice()) ? loadAmount.multiply(kwtLogisticsOrder.getPrice()) :
+                    BigDecimal.ZERO;
+           // logisticsOrderResp.setLoadAmount(loadAmount.setScale(2, RoundingMode.HALF_UP).toPlainString() + kwtLogisticsOrder.getUnit());
+            logisticsOrderResp.setUnloadAmount("-");
+           // logisticsOrderResp.setActualTransportPrice(transportPrice.setScale(2, RoundingMode.HALF_UP).toPlainString() + priceUnit);
+
+        } else if (org.apache.commons.lang3.StringUtils.equals(kwtLogisticsOrder.getBillingMode(),
+                DictEnum.CHARGING_TYPE_2.getValue())) {
+            loadAmount = subtasks.stream().filter(x->Objects.nonNull(x.getLoadAmount()))
+                    .map(KwtWaybillOrderSubtask::getLoadAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            unloadAmount = subtasks.stream().filter(x->Objects.nonNull(x.getUnloadAmount()))
+                    .map(KwtWaybillOrderSubtask::getUnloadAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            transportPrice = Objects.nonNull(kwtLogisticsOrder.getPrice()) ? unloadAmount.multiply(kwtLogisticsOrder.getPrice()) :
+                    BigDecimal.ZERO;
+
+           // logisticsOrderResp.setLoadAmount(loadAmount.setScale(2, RoundingMode.HALF_UP).toPlainString() + kwtLogisticsOrder.getUnit());
+            logisticsOrderResp.setUnloadAmount(unloadAmount.setScale(2, RoundingMode.HALF_UP).toPlainString() + kwtLogisticsOrder.getUnit());
+            //logisticsOrderResp.setActualTransportPrice(transportPrice.setScale(2, RoundingMode.HALF_UP).toPlainString() + priceUnit);
+        }
+        logisticsOrderResp.setLoadAmount(loadAmount.setScale(2, RoundingMode.HALF_UP).toPlainString() + kwtLogisticsOrder.getUnit());
+        logisticsOrderResp.setActualTransportPrice(transportPrice.setScale(2, RoundingMode.HALF_UP).toPlainString() + priceUnit);
+//        logisticsOrderResp.setActualTransportPrice(actualTransportPrice.setScale(2, RoundingMode.HALF_UP).toPlainString() + priceUnit);
+//        logisticsOrderResp.setLoadAmount((Objects.nonNull(kwtLogisticsOrder.getLoadAmount()) ?
+//                kwtLogisticsOrder.getLoadAmount().setScale(2, RoundingMode.HALF_UP).toPlainString() : "0.00") + kwtLogisticsOrder.getUnit());
+//        logisticsOrderResp.setUnloadAmount((Objects.nonNull(kwtLogisticsOrder.getUnloadAmount()) ?
+//                kwtLogisticsOrder.getUnloadAmount().setScale(2, RoundingMode.HALF_UP).toPlainString() : "0.00") + kwtLogisticsOrder.getUnit());
 
         KwtLogisticsOrderAddress loadAdd = finalLogisticsOrderIdAndUnitTypeKeyAndAddressMap.getOrDefault(kwtLogisticsOrder.getId() + "-" + AddressTypeEnum.SHIPMENT.getCode(), new KwtLogisticsOrderAddress());
         logisticsOrderResp.setLoadAddress(loadAdd.getCityName() + loadAdd.getDetailAddress());
@@ -3196,53 +3266,118 @@ public class KwtLogisticsConsignmentService {
         logisticsOrderResp.setStatus(String.valueOf(kwtLogisticsOrder.getStatus()));
         logisticsOrderResp.setStatusDesc(LogisticsOrderV1Enum.getDesc(kwtLogisticsOrder.getStatus()));
         //设置余量
-        BigDecimal orderSurplus = getSupAmount(kwtLogisticsOrder.getTOrderId(), tradeIdAndOrderDetailVoMap, tradeIdAndLogOrderList, logisticsOrderIdAndSubtaskList);
+        BigDecimal orderSurplus = getSupAmount(kwtLogisticsOrder.getTOrderId(), tradeIdAndOrderDetailVoMap, tradeIdAndLogOrderList,
+                logisticsOrderIdAndSubtaskList, waybillOrderIdAndTicketList);
         logisticsOrderResp.setOrderSurplus(orderSurplus.toPlainString() + kwtLogisticsOrder.getUnit());
         logisticsOrderResp.setRemainingAmount(orderSurplus);
         return logisticsOrderResp;
     }
 
+    /**
+     * 计算贸易订单的剩余可分配量(余量)
+     * <p>
+     * 计算逻辑:订单余量 = 贸易订单总货物量 - 已使用的量
+     * 其中“已使用的量”包括:
+     * 1. 子运单的委托量(EntrustAmount)
+     * 2. 磅单的实际装/卸货量(根据计费模式决定取装货还是卸货)
+     * </p>
+     *
+     * @param tradeOrderId                贸易订单ID
+     * @param tradeIdAndOrderDetailVoMap  贸易订单ID与详情对象的映射
+     * @param tradeIdAndLogOrderList      贸易订单ID与关联物流订单列表的映射
+     * @param logisticsOrderIdAndSubtaskList 物流订单ID与子运单列表的映射
+     * @param waybillOrderIdAndTicketList    主运单ID与磅单列表的映射
+     * @return 剩余可分配量,保留两位小数,最小为0
+     */
     private static BigDecimal getSupAmount(Long tradeOrderId,
                                            Map<Long, OrderDetailVo> tradeIdAndOrderDetailVoMap,
                                            Map<Long, List<KwtLogisticsOrder>> tradeIdAndLogOrderList,
-                                           Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList) {
-        // KwtLogisticsOrder order = logOrderMap.getOrDefault(wbOrder.getLOrderId(), new KwtLogisticsOrder());
+                                           Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList,
+                                           Map<Long, List<KwtWaybillOrderTicket>> waybillOrderIdAndTicketList) {
+        log.debug("开始计算贸易订单余量,tradeOrderId: {}", tradeOrderId);
+
+        // 1. 获取贸易订单基础信息及总货物量
         OrderDetailVo detailVo = tradeIdAndOrderDetailVoMap.getOrDefault(tradeOrderId, new OrderDetailVo());
         BigDecimal tradeAmount = Optional.ofNullable(detailVo).map(OrderDetailVo::getAmount).orElse(BigDecimal.ZERO);
+        log.debug("贸易订单总货物量: {}", tradeAmount);
+
+        // 2. 获取该贸易订单关联的所有物流订单
         List<KwtLogisticsOrder> kwtLogisticsOrders = tradeIdAndLogOrderList.get(tradeOrderId);
-        BigDecimal logTotatalAmount = BigDecimal.ZERO;
-        if (CollectionUtils.isNotEmpty(kwtLogisticsOrders)) {
-            String billingMode = kwtLogisticsOrders.get(0).getBillingMode();
+        BigDecimal usedAmount = BigDecimal.ZERO;
 
+        if (CollectionUtils.isNotEmpty(kwtLogisticsOrders)) {
+            log.debug("关联物流订单数量: {}", kwtLogisticsOrders.size());
             for (KwtLogisticsOrder kwtLogisticsOrder : kwtLogisticsOrders) {
-                List<KwtWaybillOrderSubtask> waybillOrderSubtasks = logisticsOrderIdAndSubtaskList.getOrDefault(kwtLogisticsOrder.getId(), new ArrayList<>());
-                BigDecimal loadAmountSum = waybillOrderSubtasks.stream()
-                        .filter(x -> !Arrays.asList(CarWaybillV1Enum.COMPLETED.getCode(), CarWaybillV1Enum.CANCELLED.getCode()).contains(x.getStatus()))
-                        .map(KwtWaybillOrderSubtask::getEntrustAmount)
-                        .filter(Objects::nonNull)
-                        .reduce(BigDecimal.ZERO, BigDecimal::add);
-                //logTotatalAmount = logTotatalAmount.add(loadAmountSum);
+                Long logOrderId = kwtLogisticsOrder.getId();
+                // 获取当前物流订单下的所有子运单
+                List<KwtWaybillOrderSubtask> waybillOrderSubtasks = logisticsOrderIdAndSubtaskList.getOrDefault(logOrderId, Collections.emptyList());
+                
+                if (CollectionUtils.isEmpty(waybillOrderSubtasks)) {
+                    log.debug("物流订单 {} 下无子运单,跳过", logOrderId);
+                    continue;
+                }
+
+                // 3. 根据计费模式确定磅单类型(装货或卸货)
+                // CHARGING_TYPE_1: 按装货量计费 -> 对应装货磅单 (SHIPMENT)
+                // CHARGING_TYPE_2: 按卸货量计费 -> 对应卸货磅单 (TAKE)
+                Integer ticketType;
+                String billingMode = kwtLogisticsOrder.getBillingMode();
                 if (org.apache.commons.lang3.StringUtils.equals(billingMode, DictEnum.CHARGING_TYPE_1.getValue())) {
-                    BigDecimal unloadAmountSum = waybillOrderSubtasks.stream()
-                            .filter(x -> !Objects.equals(x.getStatus(), CarWaybillV1Enum.CANCELLED.getCode()))
-                            .map(KwtWaybillOrderSubtask::getLoadAmount)
-                            .filter(Objects::nonNull)
-                            .reduce(BigDecimal.ZERO, BigDecimal::add);
-                    logTotatalAmount = logTotatalAmount.add(loadAmountSum).add(unloadAmountSum);
+                    ticketType = AddressTypeEnum.SHIPMENT.getCode();
                 } else if (org.apache.commons.lang3.StringUtils.equals(billingMode, DictEnum.CHARGING_TYPE_2.getValue())) {
-                    BigDecimal unloadAmountSum = waybillOrderSubtasks.stream()
-                            .filter(x -> !Objects.equals(x.getStatus(), CarWaybillV1Enum.CANCELLED.getCode()))
-                            .map(KwtWaybillOrderSubtask::getUnloadAmount)
-                            .filter(Objects::nonNull)
-                            .reduce(BigDecimal.ZERO, BigDecimal::add);
-                    logTotatalAmount = logTotatalAmount.add(loadAmountSum).add(unloadAmountSum);
+                    ticketType = AddressTypeEnum.TAKE.getCode();
+                } else {
+                    // 未知计费模式,记录警告并跳过该物流订单的计算,避免数据错误
+                    log.warn("余量计算跳过未知计费模式,tradeOrderId:{}, logOrderId:{}, billingMode:{}",
+                            tradeOrderId, logOrderId, billingMode);
+                    continue;
                 }
 
-                //logTotatalAmount = logTotatalAmount.add(unloadAmountSum);
+                // 4. 计算子运单委托量总和
+                // 过滤条件:对象非空、状态非取消、委托量非空
+                List<Integer> statusList = Arrays.asList(CarWaybillV1Enum.COMPLETED.getCode(), CarWaybillV1Enum.CANCELLED.getCode());
+                BigDecimal entrustAmount = waybillOrderSubtasks.stream()
+                        .filter(Objects::nonNull)
+                        .filter(subtask -> !statusList.contains(subtask.getStatus()))
+                        .map(KwtWaybillOrderSubtask::getEntrustAmount)
+                        .filter(Objects::nonNull)
+                        .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+                // 5. 计算磅单实际量总和
+                // 逻辑:子运单 -> 主运单ID -> 磅单列表 -> 过滤特定类型 -> 累加金额/数量
+                BigDecimal ticketAmount = waybillOrderSubtasks.stream()
+                        .filter(Objects::nonNull)
+                        .filter(subtask -> !Objects.equals(subtask.getStatus(), CarWaybillV1Enum.CANCELLED.getCode()))
+                        .map(KwtWaybillOrderSubtask::getWOrderId)
+                        .filter(Objects::nonNull)
+                        .flatMap(wOrderId -> waybillOrderIdAndTicketList.getOrDefault(wOrderId, Collections.emptyList()).stream())
+                        .filter(Objects::nonNull)
+                        // 只统计与计费模式匹配的磅单类型(装或卸)
+                        .filter(ticket -> Objects.equals(ticket.getType(), ticketType))
+                        .map(KwtWaybillOrderTicket::getAmount)
+                        .filter(Objects::nonNull)
+                        .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+                // 累加当前物流订单的已使用量
+                BigDecimal currentUsed = entrustAmount.add(ticketAmount);
+                usedAmount = usedAmount.add(currentUsed);
+                
+                log.debug("物流订单 {} 计算详情: 委托量={}, 磅单量={}, 计费类型={}", 
+                        logOrderId, entrustAmount, ticketAmount, ticketType);
             }
         }
-        BigDecimal subSurplus = tradeAmount.subtract(logTotatalAmount).compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ZERO : tradeAmount.subtract(logTotatalAmount);
-        return subSurplus.setScale(2, RoundingMode.HALF_UP);
+
+        // 6. 计算最终余量:总量 - 已用量,若结果小于0则置为0
+        BigDecimal surplus = tradeAmount.subtract(usedAmount);
+        if (surplus.compareTo(BigDecimal.ZERO) <= 0) {
+            log.info("贸易订单 {} 余量计算结果为负或零,重置为0。总量: {}, 已用: {}", tradeOrderId, tradeAmount, usedAmount);
+            surplus = BigDecimal.ZERO;
+        } else {
+            log.debug("贸易订单 {} 余量计算正常。总量: {}, 已用: {}, 余量: {}", tradeOrderId, tradeAmount, usedAmount, surplus);
+        }
+
+        // 保留两位小数,四舍五入
+        return surplus.setScale(2, RoundingMode.HALF_UP);
     }
 
     public LogisticsOrderDetailResp getLogisticsOrderDetail(@Valid LogisticsOrderReq req) {

+ 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);
+    }
+}

+ 102 - 32
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/app/WaybillOrderService.java

@@ -460,7 +460,8 @@ public class WaybillOrderService {
         orderResp.setStatus(Optional.ofNullable(order.getStatus()).map(String::valueOf).orElse(null));
         orderResp.setStatusDesc(LogisticsOrderV1Enum.IN_TRANSIT.getCode().equals(order.getStatus()) ? "待接单" : "未知状态");
         //设置余量
-        BigDecimal orderSurplus = getSupAmount(order.getTOrderId(), tradeIdAndOrderDetailVoMap, tradeIdAndLogOrderList, logisticsOrderIdAndSubtaskList);
+        BigDecimal orderSurplus = getSupAmount(order.getTOrderId(), tradeIdAndOrderDetailVoMap, tradeIdAndLogOrderList,
+                logisticsOrderIdAndSubtaskList);
         orderResp.setOrderSurplus(orderSurplus.toPlainString());
         orderResp.setRemainingAmount(orderSurplus);
 
@@ -765,6 +766,7 @@ public class WaybillOrderService {
                 .collect(Collectors.groupingBy(KwtLogisticsOrder::getTOrderId));
         Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList = orderSubtaskList.stream()
                 .collect(Collectors.groupingBy(KwtWaybillOrderSubtask::getLOrderId));
+
         //物流订单商品
         List<KwtLogisticsOrderGoods> logOrderGoods = logisticsOrderGoodsRepository.queryByLogOrderIds(logOrderIdList);
         if (CollectionUtils.isEmpty(logOrderGoods)) {
@@ -814,7 +816,8 @@ public class WaybillOrderService {
                 billOrder -> {
                     return getWaybillOrderResp(billOrder, subtaskMap, logOrderMap, logOrderIdAndCirculateMap,
                             logOrderIdAndGoodsMap, logOrderIdAndUnitMap, logOrderIdAndAddressMap, ticketMap,
-                            tradeIdAndOrderDetailVoMap,tradeIdAndLogOrderList,logisticsOrderIdAndSubtaskList,dictValueAndDictResDtoMap, goodsIdAndGoodsMap);
+                            tradeIdAndOrderDetailVoMap,tradeIdAndLogOrderList,logisticsOrderIdAndSubtaskList,
+                            dictValueAndDictResDtoMap, goodsIdAndGoodsMap);
                 }).collect(Collectors.toList());
         return ordderList;
     }
@@ -857,7 +860,8 @@ public class WaybillOrderService {
         wbOrderResp.setChargeTypeDesc(DictEnum.getLabel(DictTypeEnum.CHARGING_TYPE.getType(), logOrder.getBillingMode()));
         //设置余量
         KwtLogisticsOrder order = logOrderMap.getOrDefault(wbOrder.getLOrderId(), new KwtLogisticsOrder());
-        BigDecimal supAmount = getSupAmount(order.getTOrderId(), tradeIdAndOrderDetailVoMap, tradeIdAndLogOrderList, logisticsOrderIdAndSubtaskList);
+        BigDecimal supAmount = getSupAmount(order.getTOrderId(), tradeIdAndOrderDetailVoMap, tradeIdAndLogOrderList,
+                logisticsOrderIdAndSubtaskList);
         wbOrderResp.setOrderSurplus(supAmount);
         //托运企业
         KwtLogisticsOrderUnit consignEnt = logOrderIdAndUnitMap.getOrDefault(wbOrder.getLOrderId() + "-" + UnitTypeEnum.CONSIGN.getCode(), new KwtLogisticsOrderUnit());
@@ -932,47 +936,113 @@ public class WaybillOrderService {
 
     }
 
-    private static BigDecimal getSupAmount(Long tradeOrderId,
-                                       Map<Long, OrderDetailVo> tradeIdAndOrderDetailVoMap,
-                                       Map<Long, List<KwtLogisticsOrder>> tradeIdAndLogOrderList,
-                                       Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList) {
-       // KwtLogisticsOrder order = logOrderMap.getOrDefault(wbOrder.getLOrderId(), new KwtLogisticsOrder());
+    /**
+     * 计算贸易订单余量
+     * 逻辑:贸易订单总量 - (所有关联物流订单下,非取消/完成状态的子运单委托量 + 对应计费模式的磅单量)
+     *
+     * @param tradeOrderId                贸易订单ID
+     * @param tradeIdAndOrderDetailVoMap  贸易订单详情映射
+     * @param tradeIdAndLogOrderList      贸易订单关联的物流订单列表映射
+     * @param logisticsOrderIdAndSubtaskList 物流订单关联的子运单列表映射
+     * @param waybillOrderIdAndTicketList    运单ID关联的磅单列表映射
+     * @return 订单余量,保留两位小数
+     */
+    private BigDecimal getSupAmount(Long tradeOrderId,
+                                    Map<Long, OrderDetailVo> tradeIdAndOrderDetailVoMap,
+                                    Map<Long, List<KwtLogisticsOrder>> tradeIdAndLogOrderList,
+                                    Map<Long, List<KwtWaybillOrderSubtask>> logisticsOrderIdAndSubtaskList) {
+        log.debug("开始计算贸易订单余量,tradeOrderId: {}", tradeOrderId);
+
+        // 1. 获取贸易订单总货物量
         OrderDetailVo detailVo = tradeIdAndOrderDetailVoMap.getOrDefault(tradeOrderId, new OrderDetailVo());
         BigDecimal tradeAmount = Optional.ofNullable(detailVo).map(OrderDetailVo::getAmount).orElse(BigDecimal.ZERO);
+        log.debug("贸易订单[{}]总货物量: {}", tradeOrderId, tradeAmount);
+
+        // 2. 获取该贸易订单下的所有物流订单
         List<KwtLogisticsOrder> kwtLogisticsOrders = tradeIdAndLogOrderList.get(tradeOrderId);
-        BigDecimal logTotatalAmount = BigDecimal.ZERO;
-        if (CollectionUtils.isNotEmpty(kwtLogisticsOrders)){
-            String billingMode = kwtLogisticsOrders.get(0).getBillingMode();
+        
+        // 已占用总量初始化
+        BigDecimal usedAmount = BigDecimal.ZERO;
 
+        if (CollectionUtils.isNotEmpty(kwtLogisticsOrders)) {
+            log.debug("贸易订单[{}]关联物流订单数量: {}", tradeOrderId, kwtLogisticsOrders.size());
+            
             for (KwtLogisticsOrder kwtLogisticsOrder : kwtLogisticsOrders) {
-                List<KwtWaybillOrderSubtask> waybillOrderSubtasks = logisticsOrderIdAndSubtaskList.getOrDefault(kwtLogisticsOrder.getId(), new ArrayList<>());
-                BigDecimal loadAmountSum = waybillOrderSubtasks.stream()
-                        .filter(x->!Arrays.asList(CarWaybillV1Enum.COMPLETED.getCode(), CarWaybillV1Enum.CANCELLED.getCode()).contains(x.getStatus()))
+                Long logOrderId = kwtLogisticsOrder.getId();
+                // 获取当前物流订单下的所有子运单
+                List<KwtWaybillOrderSubtask> waybillOrderSubtasks = logisticsOrderIdAndSubtaskList.getOrDefault(logOrderId, Collections.emptyList());
+                
+                if (CollectionUtils.isEmpty(waybillOrderSubtasks)) {
+                    log.debug("物流订单[{}]无子运单,跳过", logOrderId);
+                    continue;
+                }
+
+                // 定义需要剔除的状态:已取消、已完成、空载(这些状态下的运单不再占用当前可接单的余量,或者其量已结算)
+                // 注意:具体业务逻辑中,通常“进行中”的运单会占用余量。这里根据原代码逻辑,剔除CANCELLED和COMPLETED。
+                //      空载待离场,实际装货量接近0,所以也要剔除
+                List<Integer> excludeStatusList = Arrays.asList(CarWaybillV1Enum.CANCELLED.getCode(), CarWaybillV1Enum.COMPLETED.getCode()
+                        , CarWaybillV1Enum.EMPTY_WAIT_LEAVE.getCode());
+
+                // 4. 计算子运单委托量合计(仅统计未取消/未完成/空载的有效子运单)
+                BigDecimal entrustAmount = waybillOrderSubtasks.stream()
+                        .filter(Objects::nonNull)
+                        .filter(subtask -> !excludeStatusList.contains(subtask.getStatus()))
                         .map(KwtWaybillOrderSubtask::getEntrustAmount)
                         .filter(Objects::nonNull)
                         .reduce(BigDecimal.ZERO, BigDecimal::add);
-                if (org.apache.commons.lang3.StringUtils.equals(billingMode, DictEnum.CHARGING_TYPE_1.getValue())){
-                    BigDecimal unloadAmountSum = waybillOrderSubtasks.stream()
-                            .filter(x->!Objects.equals(x.getStatus(),CarWaybillV1Enum.CANCELLED.getCode()))
-                            .map(KwtWaybillOrderSubtask::getLoadAmount)
-                            .filter(Objects::nonNull)
-                            .reduce(BigDecimal.ZERO, BigDecimal::add);
-                    logTotatalAmount = logTotatalAmount.add(loadAmountSum).add(unloadAmountSum);
-                }else if (org.apache.commons.lang3.StringUtils.equals(billingMode, DictEnum.CHARGING_TYPE_2.getValue())){
-                    BigDecimal unloadAmountSum = waybillOrderSubtasks.stream()
-                            .filter(x->!Objects.equals(x.getStatus(),CarWaybillV1Enum.CANCELLED.getCode()))
-                            .map(KwtWaybillOrderSubtask::getUnloadAmount)
-                            .filter(Objects::nonNull)
-                            .reduce(BigDecimal.ZERO, BigDecimal::add);
-                    logTotatalAmount = logTotatalAmount.add(loadAmountSum).add(unloadAmountSum);
-                }
+                
+                log.debug("物流订单[{}]子运单委托量合计: {}", logOrderId, entrustAmount);
+
+                // 5. 计算运单已完成的合计量
+                BigDecimal totalLoadAmount = kwtLogisticsOrders.stream().filter(Objects::nonNull)
+                        .map(KwtLogisticsOrder::getTotalLoadAmount)
+                        .filter(Objects::nonNull)
+                        .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+                log.debug("物流订单[{}]有完成的运单实际装货量合计: {}", logOrderId, totalLoadAmount);
 
-                //logTotatalAmount = logTotatalAmount.add(unloadAmountSum);
+                // 累加到总占用量
+                usedAmount = usedAmount.add(entrustAmount).add(totalLoadAmount);
             }
         }
 
-        BigDecimal subSurplus = tradeAmount.subtract(logTotatalAmount).compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ZERO : tradeAmount.subtract(logTotatalAmount);
-        return subSurplus.setScale(2, RoundingMode.HALF_UP);
+        log.debug("贸易订单[{}]总占用量: {}", tradeOrderId, usedAmount);
+
+        // 6. 计算余量:贸易订单总量 - 已占用量
+        // 如果计算结果小于等于0,则余量为0,避免负数
+        BigDecimal surplus = tradeAmount.subtract(usedAmount);
+        BigDecimal finalSurplus = surplus.compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ZERO : surplus;
+        
+        // 保留两位小数,四舍五入
+        BigDecimal result = finalSurplus.setScale(2, RoundingMode.HALF_UP);
+        log.debug("贸易订单[{}]最终余量: {}", tradeOrderId, result);
+        
+        return result;
+    }
+
+    /**
+     * 批量构建运单ID与磅单信息映射,供订单余量计算复用。
+     */
+    private Map<Long, List<KwtWaybillOrderTicket>> getWaybillOrderIdAndTicketList(List<KwtWaybillOrderSubtask> orderSubtaskList) {
+        if (CollectionUtils.isEmpty(orderSubtaskList)) {
+            return Collections.emptyMap();
+        }
+        List<Long> waybillOrderIds = orderSubtaskList.stream()
+                .filter(Objects::nonNull)
+                .map(KwtWaybillOrderSubtask::getWOrderId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(waybillOrderIds)) {
+            return Collections.emptyMap();
+        }
+        List<KwtWaybillOrderTicket> ticketList = waybillOrderTicketRepository.queryByWOrderIds(waybillOrderIds);
+        if (CollectionUtils.isEmpty(ticketList)) {
+            return Collections.emptyMap();
+        }
+        return ticketList.stream()
+                .filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(KwtWaybillOrderTicket::getWOrderId));
     }
 
     /**

+ 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);
+    }
+}