Explorar o código

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

# Conflicts:
#	sckw-modules/sckw-contract/src/main/java/com/sckw/contract/service/operateService/KwcContractLogisticsService.java
#	sckw-modules/sckw-order/src/main/java/com/sckw/order/serivce/KwoTradeOrderService.java
xucaiqin hai 1 mes
pai
achega
82a570d82a
Modificáronse 32 ficheiros con 1978 adicións e 43 borrados
  1. 7 0
      sckw-modules-api/sckw-order-api/src/main/java/com/sckw/order/api/feign/TradeOrderApi.java
  2. 33 0
      sckw-modules-api/sckw-order-api/src/main/java/com/sckw/order/api/model/OrderExecutionDisplayVo.java
  3. 32 0
      sckw-modules-api/sckw-order-api/src/main/java/com/sckw/order/api/model/RealtimeSalesGoodsSeriesVo.java
  4. 8 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/dubbo/TransportRemoteService.java
  5. 31 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/feign/TransportApi.java
  6. 27 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/dto/TradeOrderWaybillAggDto.java
  7. 26 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/CurrentCapacityAnalysisVo.java
  8. 32 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/RealtimeSalesGoodsSeriesVo.java
  9. 28 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/RealtimeSalesVolumeVo.java
  10. 59 0
      sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/TransportTopStatisticsVo.java
  11. 39 0
      sckw-modules/sckw-contract/src/main/java/com/sckw/contract/model/enums/LogisticsTransportBizTypeEnum.java
  12. 1 1
      sckw-modules/sckw-contract/src/main/java/com/sckw/contract/model/vo/req/QueryLogisticListReq.java
  13. 2 2
      sckw-modules/sckw-contract/src/main/java/com/sckw/contract/model/vo/res/QueryLogisticListResp.java
  14. 3 1
      sckw-modules/sckw-contract/src/main/java/com/sckw/contract/service/operateService/KwcContractLogisticsService.java
  15. 8 1
      sckw-modules/sckw-order/src/main/java/com/sckw/order/controller/McpController.java
  16. 97 17
      sckw-modules/sckw-order/src/main/java/com/sckw/order/serivce/KwoTradeOrderService.java
  17. 4 4
      sckw-modules/sckw-order/src/main/resources/bootstrap-cxf.yml
  18. 61 0
      sckw-modules/sckw-report/src/main/java/com/sckw/report/controller/BiDashboardController.java
  19. 88 0
      sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwCapacityAnalysisService.java
  20. 41 0
      sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwHomeService.java
  21. 33 0
      sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwOrderExecutionService.java
  22. 38 0
      sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwRealtimeSalesService.java
  23. 4 4
      sckw-modules/sckw-report/src/main/resources/bootstrap-cxf.yml
  24. 6 1
      sckw-modules/sckw-transport/pom.xml
  25. 20 3
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/McpController.java
  26. 39 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/TransportDashboardController.java
  27. 119 9
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/dubbo/TransportServiceImpl.java
  28. 17 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/repository/KwtWaybillOrderRepository.java
  29. 172 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/TransportStatisticsService.java
  30. 395 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/dashboard/RealtimeSalesVolumeService.java
  31. 443 0
      sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/dashboard/SubtaskCapacityAnalysisService.java
  32. 65 0
      sckw-modules/sckw-transport/src/test/java/com/sckw/transport/service/dashboard/RealtimeSalesVolumeServiceTest.java

+ 7 - 0
sckw-modules-api/sckw-order-api/src/main/java/com/sckw/order/api/feign/TradeOrderApi.java

@@ -1,5 +1,6 @@
 package com.sckw.order.api.feign;
 
+import com.sckw.order.api.model.OrderExecutionDisplayVo;
 import com.sckw.order.api.model.OrderPara;
 import com.sckw.order.api.model.TradeOrderVo;
 import org.springframework.cloud.openfeign.FeignClient;
@@ -22,4 +23,10 @@ public interface TradeOrderApi {
      */
     @GetMapping("/tradeOrder/trade")
     List<TradeOrderVo> trade(@SpringQueryMap OrderPara para);
+
+    /**
+     * 订单执行情况:已下单未结算订单的计划量、已装载、车次、完成率
+     */
+    @GetMapping("/tradeOrder/orderExecutionDisplay")
+    List<OrderExecutionDisplayVo> orderExecutionDisplay();
 }

+ 33 - 0
sckw-modules-api/sckw-order-api/src/main/java/com/sckw/order/api/model/OrderExecutionDisplayVo.java

@@ -0,0 +1,33 @@
+package com.sckw.order.api.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 订单执行情况看板行数据(已下单未结算)
+ */
+@Data
+public class OrderExecutionDisplayVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "订单号(贸易订单编号)")
+    private String orderNo;
+
+    @Schema(description = "计划量,单位:吨,四舍五入正整数")
+    private Integer plannedTons;
+
+    @Schema(description = "已装载,单位:吨,四舍五入两位小数(已完成运单净重汇总)")
+    private BigDecimal loadedTons;
+
+    @Schema(description = "车次:已完成车辆运单数量")
+    private Integer tripCount;
+
+    @Schema(description = "完成率(百分比数值),已装载÷计划量×100,四舍五入两位小数,如 95.56 表示 95.56%")
+    private BigDecimal completionRatePercent;
+}

+ 32 - 0
sckw-modules-api/sckw-order-api/src/main/java/com/sckw/order/api/model/RealtimeSalesGoodsSeriesVo.java

@@ -0,0 +1,32 @@
+package com.sckw.order.api.model;
+
+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;
+
+/**
+ * 实时销量:单个商品在各时间点的净重(吨)
+ */
+@Data
+public class RealtimeSalesGoodsSeriesVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "商品id")
+    private Long goodsId;
+
+    @Schema(description = "商品名称(含规格等展示名)")
+    private String goodsName;
+
+    /**
+     * 与 {@link RealtimeSalesVolumeVo#timeLabels} 下标一一对应,单位:吨
+     */
+    @Schema(description = "各时间点净重汇总(吨),与 timeLabels 顺序一致")
+    private List<BigDecimal> tonsPerSlot = new ArrayList<>();
+}

+ 8 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/dubbo/TransportRemoteService.java

@@ -2,6 +2,7 @@ package com.sckw.transport.api.dubbo;
 
 import com.sckw.core.web.response.HttpResult;
 import com.sckw.transport.api.model.dto.AcceptCarriageLogisticsOrderDto;
+import com.sckw.transport.api.model.dto.TradeOrderWaybillAggDto;
 import com.sckw.transport.api.model.dto.AccountCheckingBindDTO;
 import com.sckw.transport.api.model.dto.RWaybillOrderDto;
 import com.sckw.transport.api.model.param.*;
@@ -214,4 +215,11 @@ public interface TransportRemoteService {
     Object countLogistics(CountPara2 countPara);
 
     List<LogisticContractVo> queryLogOrderCirculateByLogOrderId(Long tradeOrderId);
+
+    /**
+     * 按贸易订单汇总:状态为「已完成」的车辆运单数量及子单净重合计(吨)
+     *
+     * @param tOrderIds 贸易订单 id 列表(建议按展示顺序传入,返回值顺序与入参一致)
+     */
+    List<TradeOrderWaybillAggDto> aggregateCompletedWaybillStatsByTradeOrderIds(List<Long> tOrderIds);
 }

+ 31 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/feign/TransportApi.java

@@ -1,11 +1,16 @@
 package com.sckw.transport.api.feign;
 
+import com.sckw.core.web.response.BaseResult;
 import com.sckw.transport.api.model.LogisticsBaseOrderVo;
 import com.sckw.transport.api.model.dto.McpLogisticsOrderVo;
 import com.sckw.transport.api.model.param.OrderPara;
+import com.sckw.transport.api.model.vo.TransportTopStatisticsVo;
+import com.sckw.transport.api.model.vo.CurrentCapacityAnalysisVo;
+import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.cloud.openfeign.SpringQueryMap;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
 
 import java.util.List;
 
@@ -32,4 +37,30 @@ public interface TransportApi {
 
     @GetMapping("/logisticsOrder/base")
     List<LogisticsBaseOrderVo> base(@SpringQueryMap OrderPara para);
+
+    /**
+     * 当前运力分析展示统计(门卫运单 + 车辆运单状态)
+     */
+    @PostMapping("/appGatekeeper/currentCapacityAnalysis")
+    BaseResult<CurrentCapacityAnalysisVo> currentCapacityAnalysis();
+
+    /**
+     * 实时销量:近 12 小时整点,每点为上小时完成运单净重(吨),按商品分线
+     */
+    @GetMapping("/dashboard/realtimeSalesVolume")
+    BaseResult<RealtimeSalesVolumeVo> realtimeSalesVolume();
+
+    /**
+     * 当前运力分析(子运单 kwt_waybill_order_subtask 口径)
+     */
+    @PostMapping("/dashboard/currentCapacityAnalysisBySubtask")
+    BaseResult<CurrentCapacityAnalysisVo> currentCapacityAnalysisBySubtask();
+
+    /**
+     * 查询首页顶部运输统计数据。
+     *
+     * @return 顶部四项统计结果
+     */
+    @GetMapping("/logisticsOrder/topStatistics")
+    TransportTopStatisticsVo topStatistics();
 }

+ 27 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/dto/TradeOrderWaybillAggDto.java

@@ -0,0 +1,27 @@
+package com.sckw.transport.api.model.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 贸易订单维度:已完成运单车次与净重汇总
+ */
+@Data
+public class TradeOrderWaybillAggDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "贸易订单id")
+    private Long tOrderId;
+
+    @Schema(description = "已完成车辆运单数量")
+    private Integer tripCount;
+
+    @Schema(description = "已完成运单净重合计(吨,未做展示层四舍五入)")
+    private BigDecimal netWeightSum;
+}

+ 26 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/CurrentCapacityAnalysisVo.java

@@ -0,0 +1,26 @@
+package com.sckw.transport.api.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 当前运力分析展示(待进场 / 装载作业 / 配送中)
+ */
+@Data
+public class CurrentCapacityAnalysisVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "待进场:已接单未进场的运输车辆实时数量,单位:辆")
+    private Integer pendingEntryCount;
+
+    @Schema(description = "装载作业:已进场未出场的运输车辆实时数量,单位:辆")
+    private Integer loadingOperationCount;
+
+    @Schema(description = "配送中:已出场未完成卸货(有卸货地址)+ 出场2小时内且无卸货地址的运输车辆数量,单位:辆")
+    private Integer deliveringCount;
+}

+ 32 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/RealtimeSalesGoodsSeriesVo.java

@@ -0,0 +1,32 @@
+package com.sckw.transport.api.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;
+
+/**
+ * 实时销量:单个商品在各时间点的净重(吨)
+ */
+@Data
+public class RealtimeSalesGoodsSeriesVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "商品id")
+    private Long goodsId;
+
+    @Schema(description = "商品名称(含规格等展示名)")
+    private String goodsName;
+
+    /**
+     * 与 {@link RealtimeSalesVolumeVo#timeLabels} 下标一一对应,单位:吨
+     */
+    @Schema(description = "各时间点净重汇总(吨),与 timeLabels 顺序一致")
+    private List<BigDecimal> tonsPerSlot = new ArrayList<>();
+}

+ 28 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/RealtimeSalesVolumeVo.java

@@ -0,0 +1,28 @@
+package com.sckw.transport.api.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 实时销量:近 12 个整点小时,每点展示「上一整小时」内完成运单的净重汇总(按商品拆线)
+ */
+@Data
+public class RealtimeSalesVolumeVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 横坐标:整点标签,最末为当前整点;该点数据区间为 [标签-1h, 标签)
+     */
+    @Schema(description = "时间轴标签,如 11:00 表示统计 10:00~11:00(左闭右开)完成运单")
+    private List<String> timeLabels = new ArrayList<>();
+
+    @Schema(description = "各商品折线数据")
+    private List<RealtimeSalesGoodsSeriesVo> series = new ArrayList<>();
+}

+ 59 - 0
sckw-modules-api/sckw-transport-api/src/main/java/com/sckw/transport/api/model/vo/TransportTopStatisticsVo.java

@@ -0,0 +1,59 @@
+package com.sckw.transport.api.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 顶部数据展示区统计结果。
+ * <p>
+ * 该对象用于承载报表首页顶部四个核心指标:
+ * 1. 累计运量
+ * 2. 累计车次
+ * 3. 今日车次
+ * 4. 今日运量
+ * </p>
+ *
+ * @author ai
+ * @date 2026/04/22
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(description = "顶部数据展示区统计结果")
+public class TransportTopStatisticsVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 系统内所有已完成运单的净重汇总。
+     */
+    @Schema(description = "累计运量")
+    private BigDecimal totalTransportAmount;
+
+    /**
+     * 系统内所有已完成运单数量。
+     */
+    @Schema(description = "累计车次")
+    private Long totalTripCount;
+
+    /**
+     * 系统内当日完成运单数量,按运单完成时间统计。
+     */
+    @Schema(description = "今日车次")
+    private Long todayTripCount;
+
+    /**
+     * 系统内当日完成运单的净重汇总,按运单完成时间统计。
+     */
+    @Schema(description = "今日运量")
+    private BigDecimal todayTransportAmount;
+}

+ 39 - 0
sckw-modules/sckw-contract/src/main/java/com/sckw/contract/model/enums/LogisticsTransportBizTypeEnum.java

@@ -0,0 +1,39 @@
+package com.sckw.contract.model.enums;
+
+import lombok.Getter;
+
+/**
+ * 物流合同业务类型(新增页「合同类型」)
+ */
+@Getter
+public enum LogisticsTransportBizTypeEnum {
+
+    /** 贸易运输:沿用原系统逻辑 */
+    TRADE_TRANSPORT(1, "贸易运输"),
+    /** 原矿转运:不录入运价,使用允许误差做装卸货量比对 */
+    RAW_ORE_TRANSFER(2, "原矿转运");
+
+    private final int code;
+    private final String label;
+
+    LogisticsTransportBizTypeEnum(int code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+
+    public static String getLabelByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (LogisticsTransportBizTypeEnum e : values()) {
+            if (e.code == code) {
+                return e.label;
+            }
+        }
+        return null;
+    }
+
+    public static boolean isRawOreTransfer(Integer code) {
+        return code != null && code == RAW_ORE_TRANSFER.code;
+    }
+}

+ 1 - 1
sckw-modules/sckw-contract/src/main/java/com/sckw/contract/model/vo/req/QueryLogisticListReq.java

@@ -44,7 +44,7 @@ public class QueryLogisticListReq extends PageReq implements Serializable {
      */
     private String status;
     /**
-     * 合同类型:1托运合同 2承运合同(相对当前查看企业)
+     * 合同类型:1贸易运输 2原矿转运(相对当前查看企业)
      */
     @Schema(description = "合同类型:1托运合同 2承运合同")
     private Integer contractType;

+ 2 - 2
sckw-modules/sckw-contract/src/main/java/com/sckw/contract/model/vo/res/QueryLogisticListResp.java

@@ -55,9 +55,9 @@ public class QueryLogisticListResp implements Serializable {
     @Schema(description = "承运单位名称")
     private String carriageUnitName;
     /**
-     * 合同类型:1托运合同 2承运合同(相对当前查看企业)
+     * 合同类型:1贸易运输 2原矿转运(相对当前查看企业)
      */
-    @Schema(description = "合同类型:1托运合同 2承运合同")
+    @Schema(description = "合同类型:1贸易运输 2原矿转运(相对当前查看企业)")
     private Integer contractType;
     /**
      * 合同类型描述

+ 3 - 1
sckw-modules/sckw-contract/src/main/java/com/sckw/contract/service/operateService/KwcContractLogisticsService.java

@@ -15,6 +15,8 @@ import com.sckw.contract.api.model.dto.res.TradeEntInfoResVo;
 import com.sckw.contract.dao.KwcContractLogisticsMapper;
 import com.sckw.contract.model.dto.req.QueryListReqDto;
 import com.sckw.contract.model.dto.res.QueryListResDto;
+import com.sckw.contract.model.entity.*;
+import com.sckw.contract.model.enums.LogisticsTransportBizTypeEnum;
 import com.sckw.contract.model.entity.KwcContractLogistics;
 import com.sckw.contract.model.entity.KwcContractLogisticsGoods;
 import com.sckw.contract.model.entity.KwcContractLogisticsUnit;
@@ -818,7 +820,7 @@ public class KwcContractLogisticsService {
             if (Objects.nonNull(queryListResVo.getEndTime())) {
                 queryListResVo.setEndTime(DateUtils.getStartOfDay(queryListResVo.getEndTime()));
                 String endDate = DateUtils.format(queryListResVo.getEndTime(), DateUtils.DATE_PATTERN);
-                if (org.apache.commons.lang3.StringUtils.equals(endDate, "9999-12-30")) {
+                if (org.apache.commons.lang3.StringUtils.equals(endDate,"9999-12-30")){
                     queryListResVo.setEndTime(null);
                 }
             }

+ 8 - 1
sckw-modules/sckw-order/src/main/java/com/sckw/order/controller/McpController.java

@@ -1,9 +1,10 @@
 package com.sckw.order.controller;
 
 import cn.hutool.core.date.LocalDateTimeUtil;
+import com.sckw.order.api.model.OrderExecutionDisplayVo;
+import com.sckw.order.api.model.OrderPara;
 import com.sckw.order.api.model.TradeOrderPara;
 import com.sckw.order.api.model.TradeOrderVo;
-import com.sckw.order.api.model.OrderPara;
 import com.sckw.order.serivce.KwoTradeOrderService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -44,4 +45,10 @@ public class McpController {
         return  kwoTradeOrderService.queryOrder(tradeOrderPara);
     }
 
+    @GetMapping(value = "/orderExecutionDisplay")
+    @Operation(summary = "订单执行情况看板", description = "已下单未结算订单的计划量、已装载、车次、完成率")
+    public List<OrderExecutionDisplayVo> orderExecutionDisplay() {
+        return kwoTradeOrderService.listOrderExecutionDisplay();
+    }
+
 }

+ 97 - 17
sckw-modules/sckw-order/src/main/java/com/sckw/order/serivce/KwoTradeOrderService.java

@@ -10,6 +10,7 @@ import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -82,6 +83,7 @@ import com.sckw.system.api.model.dto.req.ActualDisPatchDto;
 import com.sckw.system.api.model.dto.req.DataPermissionFilterReqDto;
 import com.sckw.system.api.model.dto.res.*;
 import com.sckw.transport.api.dubbo.TransportRemoteService;
+import com.sckw.transport.api.model.dto.TradeOrderWaybillAggDto;
 import com.sckw.transport.api.model.param.AddLogisticOrderParam;
 import com.sckw.transport.api.model.param.LogisticInfo;
 import com.sckw.transport.api.model.param.OrderFinishParam;
@@ -2224,7 +2226,7 @@ public class KwoTradeOrderService {
         List<TradeContractUnitDto> unitList = tradeContractResDto.getUnitList();
         List<KwoTradeOrderUnit> list = new ArrayList<>();
         WalletPrepaidDto walletFreeze = new WalletPrepaidDto();
-        walletFreeze.setOrderNo(order.getTOrderNo());
+        walletFreeze.setOrderNo(String.valueOf(order.getId()));
         walletFreeze.setTradeAmount(order.getPrice());
         unitList.forEach(e -> {
             KwoTradeOrderUnit unit = BeanUtil.copyProperties(e, KwoTradeOrderUnit.class);
@@ -2253,7 +2255,6 @@ public class KwoTradeOrderService {
             walletFreeze.setOrderType(4);
             walletFreeze.setTradeType(4);
             walletFreeze.setRemark("贸易订单号:" + walletFreeze.getOrderNo() + ",下单冻结");
-            log.info("贸易订单号:{},订单下单调用钱包服务冻结预付余额,参数:{}", walletFreeze.getOrderNo(), JSON.toJSONString(walletFreeze));
             freezeBaseResult = paymentFeignService.updatePrepaidBalance(walletFreeze);
         }
 
@@ -2293,23 +2294,23 @@ public class KwoTradeOrderService {
                 freezeDto.setSupEntId(unit.getEntId());
             }
         });
-        freezeDto.setOrderNo(order.getTOrderNo());
+        freezeDto.setOrderNo(String.valueOf(order.getId()));
         freezeDto.setOrderType(4);
         freezeDto.setTradeType(5);
         freezeDto.setRemark("贸易订单号:" + freezeDto.getOrderNo() + ",撤销订单");
         BaseResult<Object> balanceResult;
         try {
-            log.info("贸易订单号:{},订单撤销调用钱包服务解冻预付余额,参数:{}", freezeDto.getOrderNo(), JSON.toJSONString(freezeDto));
             balanceResult = paymentFeignService.updatePrepaidBalance(freezeDto);
         } catch (Exception e) {
-            String errMsg = freezeDto.getRemark() + "异常";
-            log.error(errMsg, e);
-            throw new BusinessException(errMsg);
+            log.error("线下钱包扣减预付余额、增加冻结金额异常", e);
+            throw new BusinessException("线下钱包扣减预付余额、增加冻结金额异常");
         }
-        if (balanceResult == null || balanceResult.getCode() != HttpStatus.SUCCESS_CODE || !Boolean.TRUE.equals(balanceResult.getData())) {
-            String errMsg = freezeDto.getRemark() + "失败";
-            log.error("{}:{}", errMsg, balanceResult != null ? balanceResult.getMessage() : "");
-            throw new BusinessException(errMsg);
+        if (balanceResult == null || balanceResult.getCode() != HttpStatus.SUCCESS_CODE) {
+            throw new BusinessException("线下钱包扣减预付余额、增加冻结金额失败");
+        }
+        Boolean aBoolean = (Boolean) balanceResult.getData();
+        if (!Boolean.TRUE.equals(aBoolean)) {
+            throw new BusinessException("线下钱包扣减预付余额、增加冻结金额失败");
         }
 
         // 3. 贸易合同该商品有采购数量,则更新采购数量(加回本单总量)
@@ -2433,7 +2434,7 @@ public class KwoTradeOrderService {
             //钱包退回金额
             WalletPrepaidDto walletFreeze = new WalletPrepaidDto();
 //            walletFreeze.setTTradeOrderId(kwoTradeOrder.getId());
-            walletFreeze.setOrderNo(kwoTradeOrder.getTOrderNo());
+            walletFreeze.setOrderNo(String.valueOf(kwoTradeOrder.getId()));
             walletFreeze.setProEntId(unitMap.get(String.valueOf(CooperateTypeEnum.PURCHASER.getCode())).getEntId());
             walletFreeze.setSupEntId(unitMap.get(String.valueOf(CooperateTypeEnum.SUPPLIER.getCode())).getEntId());
             walletFreeze.setOrderType(4);
@@ -2447,7 +2448,6 @@ public class KwoTradeOrderService {
 
             } else if (Objects.equals(kwoTradeOrder.getSettlement(), 2)) {
                 //预付制结算解冻
-                log.info("贸易订单号:{},订单审核拒绝调用钱包服务解冻预付余额,参数:{}", walletFreeze.getOrderNo(), JSON.toJSONString(walletFreeze));
                 booleanBaseResult = paymentFeignService.updatePrepaidBalance(walletFreeze);
             }
             if (booleanBaseResult.getCode() != 60200) {
@@ -2840,13 +2840,12 @@ public class KwoTradeOrderService {
                 }
                 //钱包退回金额
                 WalletPrepaidDto unFreezePrepaidDto = new WalletPrepaidDto();
-                unFreezePrepaidDto.setOrderNo(kwoTradeOrder.getTOrderNo());
+                unFreezePrepaidDto.setOrderNo(String.valueOf(kwoTradeOrder.getId()));
                 unFreezePrepaidDto.setProEntId(unitMap.get(String.valueOf(CooperateTypeEnum.PURCHASER.getCode())).getEntId());
                 unFreezePrepaidDto.setSupEntId(unitMap.get(String.valueOf(CooperateTypeEnum.SUPPLIER.getCode())).getEntId());
                 unFreezePrepaidDto.setOrderType(4);
                 unFreezePrepaidDto.setTradeType(5);
                 unFreezePrepaidDto.setRemark("贸易订单:" + unFreezePrepaidDto.getOrderNo() + ",");
-                log.info("贸易订单号:{},订单完结调用钱包服务解冻预付余额,参数:{}", unFreezePrepaidDto.getOrderNo(), JSON.toJSONString(unFreezePrepaidDto));
                 BaseResult<Object> unFreezeResult = paymentFeignService.updatePrepaidBalance(unFreezePrepaidDto);
                 if (unFreezeResult.getCode() != HttpStatus.SUCCESS_CODE) {
                     log.error("贸易订单号:{},订单完结解冻失败,异常信息为:{}", unFreezePrepaidDto.getOrderNo(), unFreezeResult.getMessage());
@@ -2854,14 +2853,13 @@ public class KwoTradeOrderService {
                 }
                 //2计算订单金额
                 WalletPrepaidDto consumePrepaidDto = new WalletPrepaidDto();
-                unFreezePrepaidDto.setOrderNo(kwoTradeOrder.getTOrderNo());
+                unFreezePrepaidDto.setOrderNo(String.valueOf(kwoTradeOrder.getId()));
                 unFreezePrepaidDto.setProEntId(unitMap.get(String.valueOf(CooperateTypeEnum.PURCHASER.getCode())).getEntId());
                 unFreezePrepaidDto.setSupEntId(unitMap.get(String.valueOf(CooperateTypeEnum.SUPPLIER.getCode())).getEntId());
                 unFreezePrepaidDto.setOrderType(4);
                 unFreezePrepaidDto.setTradeType(6);
                 consumePrepaidDto.setTradeAmount(kwoTradeOrder.getPrice());
                 consumePrepaidDto.setRemark("贸易订单号:" + consumePrepaidDto.getOrderNo() + ",订单完结消费");
-                log.info("贸易订单号:{},订单完结调用钱包服务消费预付余额,参数:{}", consumePrepaidDto.getOrderNo(), JSON.toJSONString(consumePrepaidDto));
                 BaseResult<Object> consumeResult = paymentFeignService.updatePrepaidBalance(consumePrepaidDto);
                 if (consumeResult.getCode() != HttpStatus.SUCCESS_CODE) {
                     log.error("贸易订单号:{},订单完结消费失败,异常信息为:{}", consumePrepaidDto.getOrderNo(), consumeResult.getMessage());
@@ -2926,6 +2924,88 @@ public class KwoTradeOrderService {
         return new ArrayList<>();
     }
 
+
+    /**
+     * 订单执行情况:已下单未结算({@link TradeOrderStatusEnum}:待审核、进行中、结算中;不含已完成、审核驳回、已取消),最多 500 条,按更新时间倒序
+     *
+     * @return 订单执行展示列表
+     */
+    public List<OrderExecutionDisplayVo> listOrderExecutionDisplay() {
+        log.info("开始查询订单执行情况展示列表");
+
+        // 1. 查询符合条件的贸易订单列表(待审核、进行中、结算中),限制最多500条,按更新时间倒序
+        List<KwoTradeOrder> orders = kwoTradeOrderMapper.selectList(
+                Wrappers.<KwoTradeOrder>lambdaQuery()
+                        .eq(KwoTradeOrder::getDelFlag, Global.NO)
+                        .in(KwoTradeOrder::getStatus, Arrays.asList(
+                                TradeOrderStatusEnum.AUDIT.getCode(),
+                                TradeOrderStatusEnum.ING.getCode(),
+                                TradeOrderStatusEnum.DEAL.getCode()))
+                        .orderByDesc(KwoTradeOrder::getUpdateTime)
+                        .last("LIMIT 500"));
+
+        if (CollectionUtils.isEmpty(orders)) {
+            log.info("未查询到符合条件的订单数据");
+            return Collections.emptyList();
+        }
+        log.info("查询到 {} 条待处理订单", orders.size());
+
+        // 2. 提取订单ID列表,批量查询物流运单聚合统计数据
+        List<Long> ids = orders.stream().map(KwoTradeOrder::getId).toList();
+        log.debug("开始批量查询物流运单统计信息,订单ID数量: {}", ids.size());
+        List<TradeOrderWaybillAggDto> aggs = transportRemoteService.aggregateCompletedWaybillStatsByTradeOrderIds(ids);
+
+        // 3. 将物流统计数据转换为Map,方便后续通过订单ID快速查找
+        Map<Long, TradeOrderWaybillAggDto> aggMap = aggs.stream()
+                .collect(Collectors.toMap(TradeOrderWaybillAggDto::getTOrderId, Function.identity(), (a, b) -> a));
+        log.debug("获取到 {} 条物流运单统计信息", aggMap.size());
+
+        // 4. 组装返回结果
+        List<OrderExecutionDisplayVo> rows = new ArrayList<>(orders.size());
+        for (KwoTradeOrder o : orders) {
+            TradeOrderWaybillAggDto a = aggMap.get(o.getId());
+
+            // 获取运输趟次,若无数据则默认为0
+            int trips = a != null && a.getTripCount() != null ? a.getTripCount() : 0;
+
+            // 获取已装载净重总和,若无数据则默认为0
+            BigDecimal loadedRaw = a != null && a.getNetWeightSum() != null ? a.getNetWeightSum() : BigDecimal.ZERO;
+
+            // 获取订单计划总量,若无数据则默认为0
+            BigDecimal plannedRaw = o.getAmount() != null ? o.getAmount() : BigDecimal.ZERO;
+
+            // 计算计划吨数(取整)
+            int plannedTons = Math.max(0, plannedRaw.setScale(0, RoundingMode.HALF_UP).intValue());
+
+            // 计算已装载吨数(保留两位小数)
+            BigDecimal loadedTons = loadedRaw.setScale(2, RoundingMode.HALF_UP);
+
+            // 计算完成率百分比
+            BigDecimal rate;
+            if (plannedRaw.compareTo(BigDecimal.ZERO) <= 0) {
+                // 如果计划量为0或负数,完成率为0
+                rate = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+            } else {
+                // 完成率 = (已装载量 / 计划量) * 100,保留两位小数
+                rate = loadedRaw.divide(plannedRaw, 10, RoundingMode.HALF_UP)
+                        .multiply(BigDecimal.valueOf(100))
+                        .setScale(2, RoundingMode.HALF_UP);
+            }
+
+            // 构建返回对象
+            OrderExecutionDisplayVo vo = new OrderExecutionDisplayVo();
+            vo.setOrderNo(o.getTOrderNo());
+            vo.setPlannedTons(plannedTons);
+            vo.setLoadedTons(loadedTons);
+            vo.setTripCount(trips);
+            vo.setCompletionRatePercent(rate);
+            rows.add(vo);
+        }
+
+        log.info("订单执行情况展示列表组装完成,共 {} 条数据", rows.size());
+        return rows;
+    }
+
     /**
      * 查询增补运力物流合同(手动派车物流合同)
      *

+ 4 - 4
sckw-modules/sckw-order/src/main/resources/bootstrap-cxf.yml

@@ -3,16 +3,16 @@ spring:
     nacos:
       discovery:
         # 服务注册地址
-        server-addr: @nacos.server@
+        server-addr: 118.116.4.155:8848
         # 命名空间
-        namespace: @nacos.namespace@
+        namespace: sckw-ng-service-platform-xf
         # 共享配置
         group: sckw-ng-service-platform
       config:
         # 配置中心地址
-        server-addr: @nacos.server@
+        server-addr: 118.116.4.155:8848
         # 命名空间
-        namespace: @nacos.namespace@
+        namespace: sckw-ng-service-platform-xf
         # 共享配置
         group: sckw-ng-service-platform
         # 配置文件格式

+ 61 - 0
sckw-modules/sckw-report/src/main/java/com/sckw/report/controller/BiDashboardController.java

@@ -0,0 +1,61 @@
+package com.sckw.report.controller;
+
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.order.api.model.OrderExecutionDisplayVo;
+import com.sckw.report.service.KwCapacityAnalysisService;
+import com.sckw.report.service.KwHomeService;
+import com.sckw.report.service.KwOrderExecutionService;
+import com.sckw.report.service.KwRealtimeSalesService;
+import com.sckw.transport.api.model.vo.CurrentCapacityAnalysisVo;
+import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
+import com.sckw.transport.api.model.vo.TransportTopStatisticsVo;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@AllArgsConstructor
+@RestController
+@RequestMapping("/kwBiCapacityAnalysis")
+@Tag(name = "BI Dashboard", description = "BI Dashboard")
+public class BiDashboardController {
+    private final KwHomeService kwHomeService;
+    private final KwCapacityAnalysisService capacityAnalysisService;
+    private final KwOrderExecutionService orderExecutionService;
+    private final KwRealtimeSalesService realtimeSalesService;
+
+    @GetMapping("/transportTopStatistics")
+    @Operation(summary = "运输顶部统计", description = "总运量, 总趟次, 今日趟次, 今日运量")
+    public BaseResult<TransportTopStatisticsVo> transportTopStatistics() {
+        return BaseResult.success(kwHomeService.transportTopStatistics());
+    }
+
+    @PostMapping("/currentDisplay")
+    @Operation(summary = "当前运力展示", description = "待命, 装货和配送中")
+    public BaseResult<CurrentCapacityAnalysisVo> currentDisplay() {
+        return BaseResult.success(capacityAnalysisService.currentCapacityAnalysis());
+    }
+
+    @PostMapping("/currentDisplayBySubtask")
+    @Operation(summary = "按子任务划分的当前运力", description = "按子任务划分的待命, 装货和配送中")
+    public BaseResult<CurrentCapacityAnalysisVo> currentDisplayBySubtask() {
+        return BaseResult.success(capacityAnalysisService.currentCapacityAnalysisBySubtask());
+    }
+
+    @GetMapping("/display")
+    @Operation(summary = "订单执行展示", description = "订单执行指标和列表")
+    public BaseResult<List<OrderExecutionDisplayVo>> display() {
+        return BaseResult.success(orderExecutionService.orderExecutionDisplay());
+    }
+
+    @GetMapping("/volume")
+    @Operation(summary = "实时销量", description = "实时销量趋势")
+    public BaseResult<RealtimeSalesVolumeVo> volume() {
+        return BaseResult.success(realtimeSalesService.realtimeSalesVolume());
+    }
+}

+ 88 - 0
sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwCapacityAnalysisService.java

@@ -0,0 +1,88 @@
+package com.sckw.report.service;
+
+import com.sckw.core.exception.SystemException;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.transport.api.feign.TransportApi;
+import com.sckw.transport.api.model.vo.CurrentCapacityAnalysisVo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 库位容量分析服务类
+ * 提供获取当前库位容量分析数据的相关业务逻辑
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KwCapacityAnalysisService {
+
+    private final TransportApi transportApi;
+
+    /**
+     * 获取当前库位容量分析数据
+     *
+     * @return 当前库位容量分析视图对象
+     * @throws SystemException 当远程调用失败或返回异常时抛出系统异常
+     */
+    public CurrentCapacityAnalysisVo currentCapacityAnalysis() {
+        BaseResult<CurrentCapacityAnalysisVo> remote;
+        try {
+            // 调用远程接口获取当前库位容量分析数据
+            remote = transportApi.currentCapacityAnalysis();
+        } catch (Exception e) {
+            // 记录远程调用失败的错误日志
+            log.error("调用当前库位容量分析接口失败", e);
+            throw new SystemException("获取当前库位容量分析失败: " + e.getMessage());
+        }
+        
+        // 检查远程响应是否为空
+        if (remote == null) {
+            log.error("获取当前库位容量分析失败: 远程响应为空");
+            throw new SystemException("获取当前库位容量分析失败: 远程响应为空");
+        }
+        
+        // 检查远程调用是否成功
+        if (remote.getCode() != HttpStatus.SUCCESS_CODE) {
+            String errorMsg = remote.getMessage() != null ? remote.getMessage() : "远程调用失败";
+            log.error("获取当前库位容量分析失败, 错误码: {}, 错误信息: {}", remote.getCode(), errorMsg);
+            throw new SystemException(remote.getCode(), errorMsg);
+        }
+        
+        return remote.getData();
+    }
+
+    /**
+     * 按子任务获取当前库位容量分析数据
+     *
+     * @return 当前库位容量分析视图对象
+     * @throws SystemException 当远程调用失败或返回异常时抛出系统异常
+     */
+    public CurrentCapacityAnalysisVo currentCapacityAnalysisBySubtask() {
+        BaseResult<CurrentCapacityAnalysisVo> remote;
+        try {
+            // 调用远程接口按子任务获取当前库位容量分析数据
+            remote = transportApi.currentCapacityAnalysisBySubtask();
+        } catch (Exception e) {
+            // 记录远程调用失败的错误日志
+            log.error("调用按子任务获取当前库位容量分析接口失败", e);
+            throw new SystemException("按子任务获取当前库位容量分析失败: " + e.getMessage());
+        }
+        
+        // 检查远程响应是否为空
+        if (remote == null) {
+            log.error("按子任务获取当前库位容量分析失败: 远程响应为空");
+            throw new SystemException("按子任务获取当前库位容量分析失败: 远程响应为空");
+        }
+        
+        // 检查远程调用是否成功
+        if (remote.getCode() != HttpStatus.SUCCESS_CODE) {
+            String errorMsg = remote.getMessage() != null ? remote.getMessage() : "远程调用失败";
+            log.error("按子任务获取当前库位容量分析失败, 错误码: {}, 错误信息: {}", remote.getCode(), errorMsg);
+            throw new SystemException(remote.getCode(), errorMsg);
+        }
+        
+        return remote.getData();
+    }
+}

+ 41 - 0
sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwHomeService.java

@@ -16,17 +16,20 @@ import com.sckw.report.service.vo.OperationCountVo;
 import com.sckw.report.api.model.TodoCountVo;
 import com.sckw.system.api.RemoteSystemService;
 import com.sckw.transport.api.dubbo.TransportRemoteService;
+import com.sckw.transport.api.feign.TransportApi;
 import com.sckw.transport.api.model.vo.LogisticsDto;
 import com.sckw.transport.api.model.param.LogisticsOrderAuditPara;
 import com.sckw.transport.api.model.param.TimePara;
 import com.sckw.transport.api.model.vo.CountPara2;
 import com.sckw.transport.api.model.vo.LogisticsVo;
+import com.sckw.transport.api.model.vo.TransportTopStatisticsVo;
 import com.sckw.transport.api.model.vo.WaybillOrderDetailVo;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -40,6 +43,8 @@ import java.util.List;
 @RequiredArgsConstructor
 public class KwHomeService {
 
+    private final TransportApi transportApi;
+
     @DubboReference(version = "1.0.0", group = "design", check = false)
     private RemoteSystemService remoteSystemService;
 
@@ -139,6 +144,42 @@ public class KwHomeService {
         return transportRemoteService.auditLogisticsTask(para);
     }
 
+    /**
+     * 查询首页顶部运输统计数据。
+     * <p>
+     * 现有 Dubbo 接口只能提供部分数量统计,无法同时满足:
+     * 累计运量、累计车次、今日车次、今日运量四个口径,
+     * 因此这里通过 Feign 复用运输服务新增的聚合接口,避免在报表模块重复拼装业务逻辑。
+     * </p>
+     *
+     * @return 顶部四项统计指标
+     */
+    public TransportTopStatisticsVo transportTopStatistics() {
+        log.info("开始调用运输服务查询顶部运输统计数据");
+        try {
+            TransportTopStatisticsVo statisticsVo = transportApi.topStatistics();
+            if (statisticsVo == null) {
+                log.warn("运输服务返回顶部运输统计数据为空,使用默认零值返回");
+                return buildDefaultTransportTopStatistics();
+            }
+            log.info("顶部运输统计数据查询完成,result={}", statisticsVo);
+            return statisticsVo;
+        } catch (Exception e) {
+            // 顶部卡片属于展示型接口,远程服务异常时降级为零值,避免页面整体不可用。
+            log.error("调用运输服务查询顶部运输统计数据失败,使用默认零值降级返回", e);
+            return buildDefaultTransportTopStatistics();
+        }
+    }
+
+    private TransportTopStatisticsVo buildDefaultTransportTopStatistics() {
+        return TransportTopStatisticsVo.builder()
+                .totalTransportAmount(BigDecimal.ZERO)
+                .totalTripCount(0L)
+                .todayTripCount(0L)
+                .todayTransportAmount(BigDecimal.ZERO)
+                .build();
+    }
+
     public TodoCountVo todoCount() {
         ContractCountVo contractCountVo = remoteContractService.contractCount();
         Long aLong = tradeOrderInfoService.orderCount();

+ 33 - 0
sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwOrderExecutionService.java

@@ -0,0 +1,33 @@
+package com.sckw.report.service;
+
+import com.sckw.core.exception.SystemException;
+import com.sckw.order.api.feign.TradeOrderApi;
+import com.sckw.order.api.model.OrderExecutionDisplayVo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 订单执行情况看板(聚合订单服务)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KwOrderExecutionService {
+
+    private final TradeOrderApi tradeOrderApi;
+
+    /**
+     * 已下单未结算订单执行列表(前端超过 9 条自行向上轮播)。
+     */
+    public List<OrderExecutionDisplayVo> orderExecutionDisplay() {
+        try {
+            return tradeOrderApi.orderExecutionDisplay();
+        } catch (Exception e) {
+            log.error("调用订单服务「订单执行情况」失败", e);
+            throw new SystemException("获取订单执行情况失败:" + e.getMessage());
+        }
+    }
+}

+ 38 - 0
sckw-modules/sckw-report/src/main/java/com/sckw/report/service/KwRealtimeSalesService.java

@@ -0,0 +1,38 @@
+package com.sckw.report.service;
+
+import com.sckw.core.exception.SystemException;
+import com.sckw.core.web.constant.HttpStatus;
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.transport.api.feign.TransportApi;
+import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 实时销量(聚合运输服务)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KwRealtimeSalesService {
+
+    private final TransportApi transportApi;
+
+    public RealtimeSalesVolumeVo realtimeSalesVolume() {
+        BaseResult<RealtimeSalesVolumeVo> remote;
+        try {
+            remote = transportApi.realtimeSalesVolume();
+        } catch (Exception e) {
+            log.error("调用运输服务「实时销量」失败", e);
+            throw new SystemException("获取实时销量失败:" + e.getMessage());
+        }
+        if (remote == null) {
+            throw new SystemException("获取实时销量失败:远程无响应");
+        }
+        if (remote.getCode() != HttpStatus.SUCCESS_CODE) {
+            throw new SystemException(remote.getCode(), remote.getMessage() != null ? remote.getMessage() : "远程调用失败");
+        }
+        return remote.getData();
+    }
+}

+ 4 - 4
sckw-modules/sckw-report/src/main/resources/bootstrap-cxf.yml

@@ -3,16 +3,16 @@ spring:
     nacos:
       discovery:
         # 服务注册地址
-        server-addr: @nacos.server@
+        server-addr: 118.116.4.155:8848
         # 命名空间
-        namespace: @nacos.namespace@
+        namespace: sckw-ng-service-platform-xf
         # 共享配置
         group: sckw-ng-service-platform
       config:
         # 配置中心地址
-        server-addr: @nacos.server@
+        server-addr: 118.116.4.155:8848
         # 命名空间
-        namespace: @nacos.namespace@
+        namespace: sckw-ng-service-platform-xf
         # 共享配置
         group: sckw-ng-service-platform
         # 配置文件格式

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

@@ -152,6 +152,11 @@
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
 
         <dependency>
             <groupId>com.sckw</groupId>
@@ -179,4 +184,4 @@
             </plugin>
         </plugins>
     </build>
-</project>
+</project>

+ 20 - 3
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/McpController.java

@@ -4,14 +4,16 @@ import cn.hutool.core.date.LocalDateTimeUtil;
 import com.sckw.transport.api.model.LogisticsBaseOrderVo;
 import com.sckw.transport.api.model.dto.McpLogisticsOrderVo;
 import com.sckw.transport.api.model.param.OrderPara;
+import com.sckw.transport.api.model.vo.TransportTopStatisticsVo;
 import com.sckw.transport.model.LogisticsOrderPara;
 import com.sckw.transport.service.KwtLogisticsConsignmentService;
+import com.sckw.transport.service.TransportStatisticsService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -32,8 +34,8 @@ import java.util.List;
 @Tag(name = "物流订单相关接口")
 public class McpController {
 
-    @Autowired
-    KwtLogisticsConsignmentService logisticsConsignmentService;
+    private final KwtLogisticsConsignmentService logisticsConsignmentService;
+    private final TransportStatisticsService transportStatisticsService;
 
 
     @PostMapping(value = "/list")
@@ -58,4 +60,19 @@ public class McpController {
         return logisticsConsignmentService.queryBaseList(tradeOrderPara);
     }
 
+    /**
+     * 首页顶部运输统计。
+     * <p>
+     * 提供给报表模块通过 Feign 调用,统一返回:
+     * 累计运量、累计车次、今日车次、今日运量。
+     * </p>
+     *
+     * @return 顶部统计结果
+     */
+    @GetMapping("/topStatistics")
+    @Operation(summary = "首页顶部运输统计", description = "累计运量/累计车次/今日车次/今日运量")
+    public TransportTopStatisticsVo topStatistics() {
+        return transportStatisticsService.queryTopStatistics();
+    }
+
 }

+ 39 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/controller/TransportDashboardController.java

@@ -0,0 +1,39 @@
+package com.sckw.transport.controller;
+
+import com.sckw.core.web.response.BaseResult;
+import com.sckw.transport.api.model.vo.CurrentCapacityAnalysisVo;
+import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
+import com.sckw.transport.service.dashboard.RealtimeSalesVolumeService;
+import com.sckw.transport.service.dashboard.SubtaskCapacityAnalysisService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 数据看板类开放接口(供报表等 Feign 调用)
+ */
+@RestController
+@RequestMapping("/dashboard")
+@RequiredArgsConstructor
+@Tag(name = "运输看板", description = "统计类接口")
+public class TransportDashboardController {
+
+    private final RealtimeSalesVolumeService realtimeSalesVolumeService;
+    private final SubtaskCapacityAnalysisService subtaskCapacityAnalysisService;
+
+    @Operation(summary = "实时销量", description = "近12个整点,每点统计上一小时完成运单净重(吨),按商品分线")
+    @GetMapping("/realtimeSalesVolume")
+    public BaseResult<RealtimeSalesVolumeVo> realtimeSalesVolume() {
+        return BaseResult.success(realtimeSalesVolumeService.build());
+    }
+
+    @Operation(summary = "当前运力分析(子运单口径)", description = "基于 kwt_waybill_order_subtask 聚合运单维度:待进场、装载作业、配送中")
+    @PostMapping("/currentCapacityAnalysisBySubtask")
+    public BaseResult<CurrentCapacityAnalysisVo> currentCapacityAnalysisBySubtask() {
+        return BaseResult.success(subtaskCapacityAnalysisService.analyze());
+    }
+}

+ 119 - 9
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/dubbo/TransportServiceImpl.java

@@ -43,6 +43,7 @@ import com.sckw.system.api.RemoteSystemService;
 import com.sckw.system.api.model.dto.res.SysDictResDto;
 import com.sckw.transport.api.dubbo.TransportRemoteService;
 import com.sckw.transport.api.model.dto.AcceptCarriageLogisticsOrderDto;
+import com.sckw.transport.api.model.dto.TradeOrderWaybillAggDto;
 import com.sckw.transport.api.model.dto.AccountCheckingBindDTO;
 import com.sckw.transport.api.model.dto.RWaybillOrderDto;
 import com.sckw.transport.api.model.param.*;
@@ -162,13 +163,7 @@ public class TransportServiceImpl implements TransportRemoteService {
             CarWaybillV1Enum.PENDING_VEHICLE.getCode(),
             CarWaybillV1Enum.REFUSE_TRAFFIC.getCode(),
             CarWaybillV1Enum.EXIT_COMPLETED.getCode(),
-            CarWaybillV1Enum.EMPTY_WAIT_LEAVE.getCode(),
-            CarWaybillV1Enum.WAIT_LEAVE.getCode(),
-            CarWaybillV1Enum.UNLOADING.getCode(),
-            CarWaybillV1Enum.WAIT_RELEASE.getCode(),
-            CarWaybillV1Enum.REPLENISHING.getCode(),
-            CarWaybillV1Enum.REPLENISH_FINISH.getCode(),
-            CarWaybillV1Enum.RELEASED_NOT_EXITED.getCode(),
+            CarWaybillV1Enum.WEIGHT_TRAFFIC.getCode(),
             CarWaybillV1Enum.WAIT_LOADING.getCode(),
             CarWaybillV1Enum.REVIEW_REJECTION.getCode()
     );
@@ -510,7 +505,7 @@ public class TransportServiceImpl implements TransportRemoteService {
         busSum.setBusSumType(BusinessTypeEnum.LOGISTICS_ORDER_TYPE.getName());
         busSum.setMethod(NumberConstant.TWO);
         busSum.setObject(order);
-        streamBridge.send("sckw-busSum", com.alibaba.fastjson2.JSON.toJSONString(busSum));
+        streamBridge.send("sckw-busSum", JSON.toJSONString(busSum));
         return HttpResult.ok();
     }
 
@@ -571,7 +566,7 @@ public class TransportServiceImpl implements TransportRemoteService {
             busSum.setBusSumType(BusinessTypeEnum.LOGISTICS_ORDER_TYPE.getName());
             busSum.setMethod(NumberConstant.TWO);
             busSum.setObject(order);
-            streamBridge.send("sckw-busSum", com.alibaba.fastjson2.JSON.toJSONString(busSum));
+            streamBridge.send("sckw-busSum", JSON.toJSONString(busSum));
         });
         return HttpResult.ok();
     }
@@ -1777,4 +1772,119 @@ public class TransportServiceImpl implements TransportRemoteService {
         }).collect(Collectors.toList());
     }
 
+    @Override
+    /**
+     * 根据贸易订单ID列表,聚合统计已完成的运单数据(趟次和净重)
+     *
+     * @param tOrderIds 贸易订单ID列表
+     * @return 贸易订单运单聚合统计DTO列表
+     */
+    public List<TradeOrderWaybillAggDto> aggregateCompletedWaybillStatsByTradeOrderIds(List<Long> tOrderIds) {
+        log.info("开始聚合统计已完成运单数据,贸易订单ID数量: {}", tOrderIds == null ? 0 : tOrderIds.size());
+        
+        // 1. 参数校验与预处理
+        if (CollectionUtils.isEmpty(tOrderIds)) {
+            log.debug("贸易订单ID列表为空,返回空列表");
+            return Collections.emptyList();
+        }
+        // 过滤空值并去重,保持原始顺序
+        List<Long> orderedIds = tOrderIds.stream().filter(Objects::nonNull).distinct().toList();
+        log.debug("处理后的贸易订单ID列表: {}", orderedIds);
+
+        // 2. 初始化结果容器,确保每个输入的贸易订单ID都有对应的返回对象,即使没有关联的运单
+        Map<Long, TradeOrderWaybillAggDto> acc = new LinkedHashMap<>(orderedIds.size());
+        for (Long id : orderedIds) {
+            TradeOrderWaybillAggDto dto = new TradeOrderWaybillAggDto();
+            dto.setTOrderId(id);
+            dto.setTripCount(0);
+            dto.setNetWeightSum(BigDecimal.ZERO);
+            acc.put(id, dto);
+        }
+
+        // 3. 查询关联的物流订单 (KwtLogisticsOrder)
+        List<KwtLogisticsOrder> logisticsOrders = logisticsOrderRepository.list(
+                Wrappers.<KwtLogisticsOrder>lambdaQuery()
+                        .in(KwtLogisticsOrder::getTOrderId, orderedIds)
+                        .eq(KwtLogisticsOrder::getDelFlag, Global.NO));
+        
+        if (CollectionUtils.isEmpty(logisticsOrders)) {
+            log.info("未找到关联的物流订单,贸易订单IDs: {}", orderedIds);
+            return new ArrayList<>(acc.values());
+        }
+        log.debug("找到关联的物流订单数量: {}", logisticsOrders.size());
+
+        // 4. 构建物流订单ID到贸易订单ID的映射关系
+        Map<Long, Long> lOrderIdToTOrderId = logisticsOrders.stream()
+                .filter(lo -> lo.getTOrderId() != null && lo.getId() != null)
+                .collect(Collectors.toMap(KwtLogisticsOrder::getId, KwtLogisticsOrder::getTOrderId, (a, b) -> a));
+        Set<Long> lOrderIds = lOrderIdToTOrderId.keySet();
+
+        // 5. 查询状态为“已完成”的运单 (KwtWaybillOrder)
+        List<KwtWaybillOrder> waybills = waybillOrderRepository.list(
+                Wrappers.<KwtWaybillOrder>lambdaQuery()
+                        .in(KwtWaybillOrder::getLOrderId, lOrderIds)
+                        .eq(KwtWaybillOrder::getStatus, CarWaybillV1Enum.COMPLETED.getCode())
+                        .eq(KwtWaybillOrder::getDelFlag, Global.NO));
+        
+        if (CollectionUtils.isEmpty(waybills)) {
+            log.info("未找到状态为已完成的运单,物流订单IDs: {}", lOrderIds);
+            return new ArrayList<>(acc.values());
+        }
+        log.debug("找到已完成的运单数量: {}", waybills.size());
+
+        // 6. 查询运单子任务 (KwtWaybillOrderSubtask) 用于计算净重
+        Set<Long> wOrderIds = waybills.stream().map(KwtWaybillOrder::getId).filter(Objects::nonNull).collect(Collectors.toSet());
+        List<KwtWaybillOrderSubtask> subtasks = waybillOrderSubtaskRepository.list(
+                Wrappers.<KwtWaybillOrderSubtask>lambdaQuery()
+                        .in(KwtWaybillOrderSubtask::getWOrderId, wOrderIds)
+                        .eq(KwtWaybillOrderSubtask::getDelFlag, Global.NO));
+        
+        // 7. 按运单ID分组子任务,并计算每个运单的总净重
+        Map<Long, BigDecimal> netByWaybill = new HashMap<>(wOrderIds.size());
+        Map<Long, List<KwtWaybillOrderSubtask>> subByWaybill = subtasks.stream()
+                .collect(Collectors.groupingBy(KwtWaybillOrderSubtask::getWOrderId));
+        
+        netByWaybill = wOrderIds.stream()
+                .collect(Collectors.toMap(
+                        Function.identity(),
+                        wid -> subByWaybill.getOrDefault(wid, Collections.emptyList()).stream()
+                                .map(TransportServiceImpl::waybillSubtaskNetWeightTon)
+                                .reduce(BigDecimal.ZERO, BigDecimal::add)
+                ));
+        log.debug("完成运单净重计算,运单数量: {}", netByWaybill.size());
+
+        // 8. 聚合统计数据:遍历运单,累加趟次和净重到对应的贸易订单
+        for (KwtWaybillOrder wo : waybills) {
+            Long tOrderId = lOrderIdToTOrderId.get(wo.getLOrderId());
+            if (tOrderId == null || !acc.containsKey(tOrderId)) {
+                // 理论上不应发生,因为运单是通过物流订单关联查出的
+                log.warn("运单 {} 无法关联到有效的贸易订单,物流订单ID: {}", wo.getId(), wo.getLOrderId());
+                continue;
+            }
+            TradeOrderWaybillAggDto dto = acc.get(tOrderId);
+            // 增加趟次计数
+            dto.setTripCount(dto.getTripCount() + 1);
+            // 累加净重
+            BigDecimal currentNetWeight = netByWaybill.getOrDefault(wo.getId(), BigDecimal.ZERO);
+            dto.setNetWeightSum(dto.getNetWeightSum().add(currentNetWeight));
+        }
+
+        List<TradeOrderWaybillAggDto> result = new ArrayList<>(acc.values());
+        log.info("完成聚合统计,返回结果数量: {}", result.size());
+        return result;
+    }
+
+    private static BigDecimal waybillSubtaskNetWeightTon(KwtWaybillOrderSubtask st) {
+        if (st == null) {
+            return BigDecimal.ZERO;
+        }
+        if (st.getUnloadAmount() != null && st.getUnloadAmount().compareTo(BigDecimal.ZERO) > 0) {
+            return st.getUnloadAmount();
+        }
+        if (st.getLoadAmount() != null && st.getLoadAmount().compareTo(BigDecimal.ZERO) > 0) {
+            return st.getLoadAmount();
+        }
+        return BigDecimal.ZERO;
+    }
+
 }

+ 17 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/repository/KwtWaybillOrderRepository.java

@@ -239,6 +239,23 @@ public class KwtWaybillOrderRepository extends ServiceImpl<KwtWaybillOrderMapper
                         .ge(KwtWaybillOrder::getUpdateTime, thirtyDaysAgo));
     }
 
+    /**
+     * 查询所有已完成运单的基础统计数据。
+     * <p>
+     * 该方法仅返回顶部统计所需字段,避免查询无关列,减少内存占用。
+     * </p>
+     *
+     * @param status 已完成状态编码
+     * @return 已完成运单列表
+     */
+    public List<KwtWaybillOrder> queryCompletedWaybillOrders(Integer status) {
+        return list(Wrappers.<KwtWaybillOrder>lambdaQuery()
+                .select(KwtWaybillOrder::getId, KwtWaybillOrder::getUpdateTime)
+                .eq(KwtWaybillOrder::getDelFlag, 0)
+                .eq(KwtWaybillOrder::getStatus, status)
+                .orderByDesc(KwtWaybillOrder::getUpdateTime));
+    }
+
     /**
      * 分页查询地图车辆列表(进行中任务)
      * @param startDate 开始日期

+ 172 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/TransportStatisticsService.java

@@ -0,0 +1,172 @@
+package com.sckw.transport.service;
+
+import cn.hutool.core.date.DateUtil;
+import com.sckw.core.common.enums.enums.DictEnum;
+import com.sckw.core.model.enums.CarWaybillV1Enum;
+import com.sckw.transport.model.KwtLogisticsOrder;
+import com.sckw.transport.api.model.vo.TransportTopStatisticsVo;
+import com.sckw.transport.model.KwtWaybillOrder;
+import com.sckw.transport.model.KwtWaybillOrderSubtask;
+import com.sckw.transport.repository.KwtLogisticsOrderRepository;
+import com.sckw.transport.repository.KwtWaybillOrderRepository;
+import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 运输统计服务。
+ * <p>
+ * 当前主要负责首页顶部数据展示区四项指标统计:
+ * 累计运量、累计车次、今日车次、今日运量。
+ * </p>
+ *
+ * @author ai
+ * @date 2026/04/22
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TransportStatisticsService {
+
+    private final KwtLogisticsOrderRepository logisticsOrderRepository;
+    private final KwtWaybillOrderRepository waybillOrderRepository;
+    private final KwtWaybillOrderSubtaskRepository waybillOrderSubtaskRepository;
+
+    /**
+     * 查询首页顶部运输统计数据。
+     * <p>
+     * 统计规则:
+     * 1. 仅统计状态为“已完成”的运单;
+     * 2. 运量根据所属物流订单计费方式动态统计:
+     *    按装货量计费取 loadAmount,按卸货量计费取 unloadAmount;
+     * 3. 今日维度按运单完成时间(主运单更新时间)落在当天范围内统计。
+     * </p>
+     *
+     * @return 顶部四项统计指标
+     */
+    public TransportTopStatisticsVo queryTopStatistics() {
+        Date todayStart = DateUtil.beginOfDay(new Date());
+        Date tomorrowStart = DateUtil.beginOfDay(DateUtil.offsetDay(new Date(), 1));
+
+        log.info("查询顶部运输统计数据,todayStart={}, tomorrowStart={}", todayStart, tomorrowStart);
+
+        List<KwtWaybillOrder> completedWaybillOrders =
+                waybillOrderRepository.queryCompletedWaybillOrders(CarWaybillV1Enum.COMPLETED.getCode());
+        if (completedWaybillOrders.isEmpty()) {
+            log.info("未查询到已完成运单,返回默认零值");
+            return buildEmptyStatistics();
+        }
+
+        // 提取已完成运单的ID列表,用于查询关联的子任务数据
+        List<Long> waybillOrderIds = completedWaybillOrders.stream()
+                .map(KwtWaybillOrder::getId)
+                .filter(Objects::nonNull)
+                .toList();
+        // 提取已完成运单关联的物流订单ID,用于查询计费方式
+        List<Long> logisticsOrderIds = completedWaybillOrders.stream()
+                .map(KwtWaybillOrder::getLOrderId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .toList();
+        log.debug("提取到 {} 个唯一的物流订单ID", logisticsOrderIds.size());
+
+        // 批量查询物流订单信息
+        List<KwtLogisticsOrder> logisticsOrders = logisticsOrderRepository.queryByLogisticsOrderIds(logisticsOrderIds);
+        log.debug("查询到 {} 条物流订单记录", logisticsOrders.size());
+
+        // 构建物流订单ID到计费方式的映射关系
+        Map<Long, String> billingModeMap = logisticsOrders.stream()
+                .filter(logisticsOrder -> Objects.nonNull(logisticsOrder.getId()))
+                .collect(Collectors.toMap(KwtLogisticsOrder::getId, KwtLogisticsOrder::getBillingMode, (left, right) -> left));
+        log.debug("构建计费方式映射完成,共 {} 条记录", billingModeMap.size());
+
+        // 根据运单ID列表批量查询子任务信息,用于后续统计净重
+        List<KwtWaybillOrderSubtask> subtasks = waybillOrderSubtaskRepository.queryByWOrderIds(waybillOrderIds);
+
+        // 先按运单维度汇总净重,后续累计和今日统计都直接复用,避免重复遍历明细数据。
+        // 其中净重按物流订单计费方式动态取值:
+        // billingMode=1 取装货量,billingMode=2 取卸货量,其它情况默认按卸货量处理。
+        Map<Long, BigDecimal> transportAmountMap = subtasks.stream()
+                .filter(subtask -> Objects.nonNull(subtask.getWOrderId()))
+                .collect(Collectors.groupingBy(
+                        KwtWaybillOrderSubtask::getWOrderId,
+                        Collectors.mapping(
+                                subtask -> resolveTransportAmount(subtask, billingModeMap.get(subtask.getLOrderId())),
+                                Collectors.reducing(BigDecimal.ZERO, Function.identity(), BigDecimal::add)
+                        )
+                ));
+
+        // 计算累计运量:遍历所有已完成运单,从预计算的运量映射中获取各运单净重并累加
+        BigDecimal totalTransportAmount = completedWaybillOrders.stream()
+                .map(KwtWaybillOrder::getId)
+                .map(id -> transportAmountMap.getOrDefault(id, BigDecimal.ZERO))
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        // 筛选今日完成的运单:根据运单更新时间判断是否落在当天时间范围内
+        List<KwtWaybillOrder> todayCompletedWaybillOrders = completedWaybillOrders.stream()
+                .filter(waybillOrder -> isBetween(waybillOrder.getUpdateTime(), todayStart, tomorrowStart))
+                .toList();
+
+        // 计算今日运量:遍历今日完成的运单,从预计算的运量映射中获取各运单净重并累加
+        BigDecimal todayTransportAmount = todayCompletedWaybillOrders.stream()
+                .map(KwtWaybillOrder::getId)
+                .map(id -> transportAmountMap.getOrDefault(id, BigDecimal.ZERO))
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        // 构建统计结果对象,填充累计车次、累计运量、今日车次、今日运量四项指标
+        TransportTopStatisticsVo statisticsVo = TransportTopStatisticsVo.builder()
+                .totalTripCount((long) completedWaybillOrders.size())
+                .totalTransportAmount(totalTransportAmount)
+                .todayTripCount((long) todayCompletedWaybillOrders.size())
+                .todayTransportAmount(todayTransportAmount)
+                .build();
+
+        log.info("顶部运输统计数据查询完成,result={}", statisticsVo);
+        return statisticsVo;
+    }
+
+    private TransportTopStatisticsVo buildEmptyStatistics() {
+        return TransportTopStatisticsVo.builder()
+                .totalTransportAmount(BigDecimal.ZERO)
+                .totalTripCount(0L)
+                .todayTripCount(0L)
+                .todayTransportAmount(BigDecimal.ZERO)
+                .build();
+    }
+
+    private BigDecimal defaultAmount(BigDecimal amount) {
+        return amount == null ? BigDecimal.ZERO : amount;
+    }
+
+    /**
+     * 根据物流订单计费方式解析运单净重。
+     *
+     * @param subtask     运单子单
+     * @param billingMode 物流订单计费方式
+     * @return 当前子单应参与统计的净重
+     */
+    private BigDecimal resolveTransportAmount(KwtWaybillOrderSubtask subtask, String billingMode) {
+        if (DictEnum.CHARGING_TYPE_1.getValue().equals(billingMode)) {
+            return defaultAmount(subtask.getLoadAmount());
+        }
+        if (DictEnum.CHARGING_TYPE_2.getValue().equals(billingMode)) {
+            return defaultAmount(subtask.getUnloadAmount());
+        }
+        return defaultAmount(subtask.getUnloadAmount());
+    }
+
+    private boolean isBetween(Date targetTime, Date startTime, Date endTime) {
+        return Objects.nonNull(targetTime)
+                && !targetTime.before(startTime)
+                && targetTime.before(endTime);
+    }
+}

+ 395 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/dashboard/RealtimeSalesVolumeService.java

@@ -0,0 +1,395 @@
+package com.sckw.transport.service.dashboard;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.sckw.core.common.enums.enums.DictEnum;
+import com.sckw.core.model.constant.Global;
+import com.sckw.core.model.enums.CarWaybillV1Enum;
+import com.sckw.core.utils.CollectionUtils;
+import com.sckw.transport.api.model.vo.RealtimeSalesGoodsSeriesVo;
+import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
+import com.sckw.transport.model.KwtLogisticsOrder;
+import com.sckw.transport.model.KwtLogisticsOrderGoods;
+import com.sckw.transport.model.KwtWaybillOrder;
+import com.sckw.transport.model.KwtWaybillOrderSubtask;
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * 实时销量服务:按小时、按商品汇总已完成运单的净重(吨)
+ * <p>
+ * 主要逻辑:
+ * 1. 确定最近12小时的时间窗口。
+ * 2. 查询该窗口内已完成的运单。
+ * 3. 关联查询运单子任务以获取净重,关联物流订单货物以获取商品信息。
+ * 4. 将每个运单的贡献映射到对应的小时时间槽中。
+ * 5. 聚合生成按商品分类的销量系列数据。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RealtimeSalesVolumeService {
+
+    /**
+     * 时区:亚洲/上海
+     */
+    private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
+
+    /**
+     * 时间格式化器:仅保留小时和分钟 (HH:mm)
+     */
+    private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("HH:mm");
+
+    /**
+     * 任务结束时间的最小有效纪元毫秒值 (2000-01-01 00:00:00 UTC),用于过滤无效时间
+     */
+    private static final long TASK_END_MIN_EPOCH_MS = 946684800000L;
+
+    private final KwtWaybillOrderRepository waybillOrderRepository;
+    private final KwtWaybillOrderSubtaskRepository waybillOrderSubtaskRepository;
+    private final KwtLogisticsOrderGoodsRepository logisticsOrderGoodsRepository;
+    private final KwtLogisticsOrderRepository logisticsOrderRepository;
+
+    /**
+     * 构建实时销量视图对象
+     *
+     * @return 包含时间标签和商品销量系列的视图对象
+     */
+    public RealtimeSalesVolumeVo build() {
+        log.debug("开始构建实时销量数据...");
+
+        // 1. 初始化时间窗口:当前时刻向前推12小时,截断到整点
+        ZonedDateTime h0 = ZonedDateTime.now(ZONE).truncatedTo(ChronoUnit.HOURS);
+        Date windowStart = Date.from(h0.minusHours(12).toInstant());
+        Date windowEnd = Date.from(h0.toInstant());
+        log.debug("计算时间窗口起点: {}, 终点: {}", windowStart, windowEnd);
+
+        // 2. 生成12个时间槽的结束时间点及其索引映射
+        // 时间槽从 h0-11h 到 h0,共12个小时段
+        List<ZonedDateTime> slotEnds = IntStream.range(0, 12)
+                .mapToObj(j -> h0.minusHours(11 - j))
+                .toList();
+        
+        // 建立时间槽结束点 -> 索引 (0-11) 的映射,方便后续快速定位
+        Map<ZonedDateTime, Integer> slotIndexByEnd = IntStream.range(0, slotEnds.size())
+                .boxed()
+                .collect(Collectors.toMap(slotEnds::get, i -> i));
+
+        // 3. 初始化返回对象并设置时间标签
+        RealtimeSalesVolumeVo vo = new RealtimeSalesVolumeVo();
+        vo.setTimeLabels(slotEnds.stream().map(z -> z.format(HM)).collect(Collectors.toCollection(ArrayList::new)));
+        log.debug("生成时间标签: {}", vo.getTimeLabels());
+
+        // 4. 查询时间窗口内状态为“已完成”且未删除的运单
+        List<KwtWaybillOrder> waybills = waybillOrderRepository.list(
+                Wrappers.<KwtWaybillOrder>lambdaQuery()
+                        .eq(KwtWaybillOrder::getStatus, CarWaybillV1Enum.COMPLETED.getCode())
+                        .eq(KwtWaybillOrder::getDelFlag, Global.NO)
+                        .ge(KwtWaybillOrder::getUpdateTime, windowStart)
+                        .lt(KwtWaybillOrder::getUpdateTime, windowEnd));
+
+        if (CollectionUtils.isEmpty(waybills)) {
+            log.info("在时间窗口 [{} - {}] 内未找到已完成的运单数据。", windowStart, windowEnd);
+            vo.setSeries(new ArrayList<>());
+            return vo;
+        }
+        log.debug("查询到符合条件的运单数量: {}", waybills.size());
+
+        // 5. 提取运单ID和物流订单ID集合,用于后续关联查询
+        Set<Long> wIds = waybills.stream().map(KwtWaybillOrder::getId).filter(Objects::nonNull).collect(Collectors.toSet());
+        Set<Long> lOrderIds = waybills.stream().map(KwtWaybillOrder::getLOrderId).filter(Objects::nonNull).collect(Collectors.toSet());
+        // 按物流订单计费方式决定净重取装货量还是卸货量,避免实时销量口径与结算口径不一致。
+        Map<Long, String> billingModeByLogOrder = billingModeByLogOrder(lOrderIds);
+
+        // 6. 查询运单子任务,并计算每个运单的净重总和
+        Map<Long, BigDecimal> netByWaybill = netWeightByWaybill(
+                waybillOrderSubtaskRepository.list(
+                        Wrappers.<KwtWaybillOrderSubtask>lambdaQuery()
+                                .in(KwtWaybillOrderSubtask::getWOrderId, wIds)
+                                .eq(KwtWaybillOrderSubtask::getDelFlag, Global.NO)),
+                billingModeByLogOrder);
+        log.debug("计算得到 {} 个运单的净重数据", netByWaybill.size());
+
+        // 7. 查询物流订单货物信息,并选取每个物流订单的主要商品
+        Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder = pickPrimaryGoods(
+                logisticsOrderGoodsRepository.queryByLogOrderIds(new ArrayList<>(lOrderIds)));
+        log.debug("匹配到 {} 个物流订单的商品信息", goodsByLogOrder.size());
+
+        // 8. 处理每个运单,将其贡献累加到对应的商品和时间槽中
+        Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap = new HashMap<>();
+        waybills.stream()
+                .flatMap(wo -> contributionStream(wo, windowStart, windowEnd, slotIndexByEnd, netByWaybill, goodsByLogOrder))
+                .forEach(c -> c.accumulate(seriesMap));
+
+        // 9. 将结果转换为列表并按商品名称排序
+        vo.setSeries(seriesMap.values().stream()
+                .sorted(Comparator.comparing(RealtimeSalesGoodsSeriesVo::getGoodsName, Comparator.nullsLast(String::compareTo)))
+                .collect(Collectors.toCollection(ArrayList::new)));
+
+        log.info("实时销量构建完成:运单 {} 条,生成商品系列 {} 个。", waybills.size(), vo.getSeries().size());
+        return vo;
+    }
+
+    /**
+     * 生成单个运单的贡献流
+     * <p>
+     * 判断运单是否在时间窗口内,确定其所属的时间槽,并关联商品信息生成贡献对象。
+     *
+     * @param wo               运单对象
+     * @param windowStart      时间窗口起始时间
+     * @param windowEnd        时间窗口结束时间
+     * @param slotIndexByEnd   时间槽结束点到索引的映射
+     * @param netByWaybill     运单ID到净重的映射
+     * @param goodsByLogOrder  物流订单ID到主要商品的映射
+     * @return 包含贡献信息的流,如果无效则返回空流
+     */
+    private static Stream<WaybillContribution> contributionStream(
+            KwtWaybillOrder wo,
+            Date windowStart,
+            Date windowEnd,
+            Map<ZonedDateTime, Integer> slotIndexByEnd,
+            Map<Long, BigDecimal> netByWaybill,
+            Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder) {
+
+        // 获取运单的实际完成时间
+        Date completedAt = completionInstant(wo);
+        
+        // 校验完成时间是否在查询窗口内
+        if (completedAt.before(windowStart) || !completedAt.before(windowEnd)) {
+            log.trace("运单 ID: {} 完成时间 {} 不在窗口 [{} - {}] 内,跳过。", wo.getId(), completedAt, windowStart, windowEnd);
+            return Stream.empty();
+        }
+
+        // 计算该完成时间所属的时间槽结束点(向上取整到下一小时整点)
+        ZonedDateTime slotEnd = ZonedDateTime.ofInstant(completedAt.toInstant(), ZONE)
+                .truncatedTo(ChronoUnit.HOURS)
+                .plusHours(1);
+        
+        // 查找时间槽索引
+        Integer slotIdx = slotIndexByEnd.get(slotEnd);
+        if (slotIdx == null) {
+            log.warn("运单 ID: {} 完成时间 {} 计算出的时间槽结束点 {} 未匹配到预定义时间槽,跳过。", wo.getId(), completedAt, slotEnd);
+            return Stream.empty();
+        }
+
+        // 获取关联的商品信息
+        return Optional.ofNullable(goodsByLogOrder.get(wo.getLOrderId()))
+                .filter(g -> g.getGoodsId() != null) // 确保商品ID有效
+                .map(g -> {
+                    BigDecimal tons = netByWaybill.getOrDefault(wo.getId(), BigDecimal.ZERO);
+                    log.trace("运单 ID: {} 贡献商品: {}, 重量: {} 吨, 时间槽索引: {}", wo.getId(), g.getGoodsName(), tons, slotIdx);
+                    return Stream.of(new WaybillContribution(
+                            g.getGoodsId(),
+                            g.getGoodsName(),
+                            slotIdx,
+                            tons));
+                })
+                .orElseGet(() -> {
+                    log.debug("运单 ID: {} (物流单: {}) 无有效关联商品或商品ID为空,跳过。", wo.getId(), wo.getLOrderId());
+                    return Stream.empty();
+                });
+    }
+
+    /**
+     * 从物流订单货物列表中选取每个物流订单的主要商品
+     * <p>
+     * 策略:如果一个物流订单有多个货物,选择ID最小的那个作为代表(通常是最早添加的或主商品)。
+     *
+     * @param goodsRows 物流订单货物列表
+     * @return 物流订单ID -> 主要商品对象的映射
+     */
+    private static Map<Long, KwtLogisticsOrderGoods> pickPrimaryGoods(List<KwtLogisticsOrderGoods> goodsRows) {
+        if (CollectionUtils.isEmpty(goodsRows)) {
+            log.debug("输入的物流订单货物列表为空,返回空映射。");
+            return Map.of();
+        }
+        
+        Map<Long, KwtLogisticsOrderGoods> result = goodsRows.stream()
+                .filter(g -> g.getLOrderId() != null) // 过滤掉物流订单ID为空的记录
+                .sorted(Comparator.comparing(KwtLogisticsOrderGoods::getId, Comparator.nullsLast(Long::compareTo))) // 按货物ID排序,确保确定性
+                .collect(Collectors.toMap(
+                        KwtLogisticsOrderGoods::getLOrderId, 
+                        g -> g, 
+                        (a, b) -> a // 冲突时保留第一个(即ID较小的)
+                ));
+        
+        log.debug("从 {} 条货物记录中提取出 {} 个物流订单的主要商品。", goodsRows.size(), result.size());
+        return result;
+    }
+
+    /**
+     * 查询物流订单计费方式映射。
+     *
+     * @param lOrderIds 物流订单ID集合
+     * @return 物流订单ID -> 计费方式
+     */
+    private Map<Long, String> billingModeByLogOrder(Set<Long> lOrderIds) {
+        if (CollectionUtils.isEmpty(lOrderIds)) {
+            log.debug("物流订单ID集合为空,跳过计费方式查询。");
+            return Map.of();
+        }
+        List<KwtLogisticsOrder> logisticsOrders = logisticsOrderRepository.queryByLogOrderIds(lOrderIds);
+        if (CollectionUtils.isEmpty(logisticsOrders)) {
+            log.warn("未查询到物流订单计费方式,lOrderIds={}", lOrderIds);
+            return Map.of();
+        }
+        Map<Long, String> result = logisticsOrders.stream()
+                .filter(order -> order != null && order.getId() != null && order.getBillingMode() != null)
+                .collect(Collectors.toMap(KwtLogisticsOrder::getId, KwtLogisticsOrder::getBillingMode, (left, right) -> left));
+        log.debug("物流订单计费方式映射构建完成,共 {} 条。", result.size());
+        return result;
+    }
+
+    /**
+     * 获取运单的完成时间
+     * <p>
+     * 优先使用 taskEndTime,如果无效(如时间为0或过小),则使用 updateTime。
+     *
+     * @param wo 运单对象
+     * @return 完成时间
+     */
+    private static Date completionInstant(KwtWaybillOrder wo) {
+        return Optional.ofNullable(wo.getTaskEndTime())
+                .filter(t -> t.getTime() > TASK_END_MIN_EPOCH_MS) // 过滤无效的早期时间戳
+                .or(() -> Optional.ofNullable(wo.getUpdateTime())) // 降级使用更新时间
+                .orElseGet(() -> {
+                    log.warn("运单 ID: {} 既无有效的任务结束时间也无更新时间,使用默认时间。", wo.getId());
+                    return new Date(0);
+                });
+    }
+
+    /**
+     * 计算每个运单的净重总和(吨)
+     *
+     * @param subtasks 运单子任务列表
+     * @return 运单ID -> 净重总和的映射
+     */
+    private static Map<Long, BigDecimal> netWeightByWaybill(List<KwtWaybillOrderSubtask> subtasks, Map<Long, String> billingModeByLogOrder) {
+        if (CollectionUtils.isEmpty(subtasks)) {
+            return Map.of();
+        }
+        
+        Map<Long, BigDecimal> result = subtasks.stream()
+                .filter(st -> st != null && st.getWOrderId() != null)
+                .collect(Collectors.groupingBy(
+                        KwtWaybillOrderSubtask::getWOrderId,
+                        Collectors.mapping(st -> subtaskNetTon(st, billingModeByLogOrder.get(st.getLOrderId())),
+                                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
+        
+        log.debug("从 {} 条子任务记录中聚合出 {} 个运单的净重。", subtasks.size(), result.size());
+        return result;
+    }
+
+    /**
+     * 计算单个子任务的净重
+     * <p>
+     * billingMode=1 按装货量计算,billingMode=2 按卸货量计算,其它缺失或异常值默认按卸货量计算。
+     *
+     * @param st 子任务对象
+     * @param billingMode 物流订单计费方式
+     * @return 净重(吨),默认为0
+     */
+    static BigDecimal subtaskNetTon(KwtWaybillOrderSubtask st, String billingMode) {
+        if (st == null) {
+            return BigDecimal.ZERO;
+        }
+        if (DictEnum.CHARGING_TYPE_1.getValue().equals(billingMode)) {
+            return validAmountOrZero(st.getLoadAmount());
+        }
+        return validAmountOrZero(st.getUnloadAmount());
+    }
+
+    /**
+     * 过滤无效重量,避免空指针和负数污染统计结果。
+     *
+     * @param amount 原始重量
+     * @return 有效重量;为空或小于等于0时返回0
+     */
+    private static BigDecimal validAmountOrZero(BigDecimal amount) {
+        return Optional.ofNullable(amount)
+                .filter(value -> value.compareTo(BigDecimal.ZERO) > 0)
+                .orElse(BigDecimal.ZERO);
+    }
+
+    /**
+     * 生成包含12个零值的列表,用于初始化每个商品的时间槽数据
+     *
+     * @return 包含12个 BigDecimal.ZERO 的列表
+     */
+    private static List<BigDecimal> twelveZeroSlots() {
+        return IntStream.range(0, 12)
+                .mapToObj(i -> BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP))
+                .collect(Collectors.toCollection(ArrayList::new));
+    }
+
+    /**
+     * 运单贡献记录
+     * <p>
+     * 封装了单个运单对某个商品在特定时间槽的重量贡献。
+     */
+    private record WaybillContribution(Long goodsId, String goodsName, int slotIndex, BigDecimal tons) {
+
+        /**
+         * 将当前贡献累加到系列映射中
+         *
+         * @param seriesMap 商品ID -> 销量系列视图的映射
+         */
+        void accumulate(Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap) {
+            // 如果该商品尚未在映射中,则创建一个新的系列对象
+            RealtimeSalesGoodsSeriesVo series = seriesMap.computeIfAbsent(goodsId, RealtimeSalesVolumeService::emptySeries);
+            
+            // 设置商品名称(如果当前系列名称为空,则更新)
+            Optional.ofNullable(goodsName)
+                    .filter(n -> !n.isEmpty())
+                    .filter(n -> series.getGoodsName() == null || series.getGoodsName().isEmpty())
+                    .ifPresent(series::setGoodsName);
+            
+            // 累加对应时间槽的重量
+            List<BigDecimal> tonsPerSlot = series.getTonsPerSlot();
+            if (slotIndex >= 0 && slotIndex < tonsPerSlot.size()) {
+                BigDecimal currentVal = tonsPerSlot.get(slotIndex);
+                BigDecimal newVal = currentVal.add(tons).setScale(4, RoundingMode.HALF_UP);
+                tonsPerSlot.set(slotIndex, newVal);
+            } else {
+                log.warn("时间槽索引 {} 超出范围 [0, {}],无法累加商品 ID: {} 的重量。", slotIndex, tonsPerSlot.size() - 1, goodsId);
+            }
+        }
+    }
+
+    /**
+     * 创建一个空的商品销量系列对象
+     *
+     * @param goodsId 商品ID
+     * @return 初始化后的系列对象
+     */
+    private static RealtimeSalesGoodsSeriesVo emptySeries(Long goodsId) {
+        RealtimeSalesGoodsSeriesVo s = new RealtimeSalesGoodsSeriesVo();
+        s.setGoodsId(goodsId);
+        s.setGoodsName(""); // 初始化为空字符串,后续会被填充
+        s.setTonsPerSlot(twelveZeroSlots()); // 初始化12个小时槽的数据为0
+        return s;
+    }
+}

+ 443 - 0
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/dashboard/SubtaskCapacityAnalysisService.java

@@ -0,0 +1,443 @@
+package com.sckw.transport.service.dashboard;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.sckw.core.model.constant.Global;
+import com.sckw.core.model.constant.NumberConstant;
+import com.sckw.core.model.enums.CarWaybillV1Enum;
+import com.sckw.core.model.enums.GatekeeperStatusEnum;
+import com.sckw.core.utils.CollectionUtils;
+import com.sckw.transport.api.model.vo.CurrentCapacityAnalysisVo;
+import com.sckw.transport.model.KwtGatekeeperWaybillOrder;
+import com.sckw.transport.model.KwtWaybillOrder;
+import com.sckw.transport.model.KwtWaybillOrderAddress;
+import com.sckw.transport.model.KwtWaybillOrderSubtask;
+import com.sckw.transport.repository.KwtGatekeeperWaybillOrderRepository;
+import com.sckw.transport.repository.KwtWaybillOrderAddressRepository;
+import com.sckw.transport.repository.KwtWaybillOrderRepository;
+import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 当前运力分析服务:以 {@code kwt_waybill_order_subtask} 为主表口径统计各运单车辆状态
+ * <p>
+ * 主要统计三类状态:
+ * 1. 待入场 (Pending Entry)
+ * 2. 装卸作业中 (Loading Operation)
+ * 3. 配送中/运输中 (Delivering)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SubtaskCapacityAnalysisService {
+
+    private final KwtWaybillOrderSubtaskRepository waybillOrderSubtaskRepository;
+    private final KwtWaybillOrderAddressRepository waybillOrderAddressRepository;
+    private final KwtGatekeeperWaybillOrderRepository gatekeeperWaybillOrderRepository;
+    private final KwtWaybillOrderRepository waybillOrderRepository;
+
+    /**
+     * 执行当前运力分析
+     * <p>
+     * 核心流程:
+     * 1. 查询有效子任务并聚合出每个运单的最大状态及关键时间点。
+     * 2. 加载辅助数据(卸货地址、门岗离场时间、主表更新时间)。
+     * 3. 根据业务规则将运单分类为:待入场、装卸中、配送中。
+     *
+     * @return 当前运力分析结果 VO
+     */
+    public CurrentCapacityAnalysisVo analyze() {
+        log.info("=== 开始执行当前运力分析 ===");
+
+        // 1. 查询所有有效的子任务(排除已删除和已取消的)
+        // 仅查询必要字段以减少内存占用:运单ID、状态、更新时间
+        log.debug("步骤1: 查询有效子任务...");
+        List<KwtWaybillOrderSubtask> subtasks = waybillOrderSubtaskRepository.list(
+                Wrappers.<KwtWaybillOrderSubtask>lambdaQuery()
+                        .select(KwtWaybillOrderSubtask::getWOrderId, KwtWaybillOrderSubtask::getStatus, KwtWaybillOrderSubtask::getUpdateTime)
+                        .eq(KwtWaybillOrderSubtask::getDelFlag, Global.NO)
+                        .ne(KwtWaybillOrderSubtask::getStatus, CarWaybillV1Enum.CANCELLED.getCode()));
+
+        CurrentCapacityAnalysisVo vo = new CurrentCapacityAnalysisVo();
+        if (CollectionUtils.isEmpty(subtasks)) {
+            log.warn("未查询到有效子任务,返回空结果");
+            vo.setPendingEntryCount(0);
+            vo.setLoadingOperationCount(0);
+            vo.setDeliveringCount(0);
+            return vo;
+        }
+        log.info("步骤1完成: 查询到 {} 条有效子任务", subtasks.size());
+
+        // 2. 按运单ID聚合子任务信息
+        // 对于每个运单,提取最大状态值以及“出场后、卸货前”窗口期的最后更新时间
+        log.debug("步骤2: 按运单ID聚合子任务信息...");
+        Map<Long, WaybillSubtaskAgg> aggByWaybill = subtasks.stream()
+                .filter(st -> st.getWOrderId() != null && st.getStatus() != null)
+                .collect(Collectors.groupingBy(
+                        KwtWaybillOrderSubtask::getWOrderId,
+                        Collectors.reducing(new WaybillSubtaskAgg(), SubtaskCapacityAnalysisService::partialAgg, SubtaskCapacityAnalysisService::mergeAgg)));
+
+        Set<Long> wOrderIds = aggByWaybill.keySet();
+        log.info("步骤2完成: 聚合得到 {} 个唯一运单ID", wOrderIds.size());
+
+        // 3. 加载辅助数据
+        log.debug("步骤3: 加载辅助数据...");
+
+        // 3.1 加载包含卸货地址的运单ID集合
+        Set<Long> hasUnloadWaybillIds = loadUnloadWaybillIds(wOrderIds);
+        log.debug("步骤3.1: 加载到 {} 个包含卸货地址的运单", hasUnloadWaybillIds.size());
+
+        // 3.2 加载门岗离场时间
+        Map<Long, Date> leaveTimeByWaybill = loadGatekeeperLeaveTime(wOrderIds);
+        log.debug("步骤3.2: 加载到 {} 个运单的门岗离场时间", leaveTimeByWaybill.size());
+
+        // 3.3 加载运单主表的更新时间
+        Map<Long, Date> waybillUpdate = loadWaybillUpdateTime(wOrderIds);
+        log.debug("步骤3.3: 加载到 {} 个运单的主表更新时间", waybillUpdate.size());
+
+        // 计算两小时前的时间点,用于判断无卸货地址运单是否处于活跃运输状态
+        Date twoHoursAgo = Date.from(Instant.now().minus(2, ChronoUnit.HOURS));
+        log.debug("步骤3.4: 计算活跃运输判定时间点: {}", twoHoursAgo);
+
+        // 4. 分类统计各运单状态
+        log.debug("步骤4: 开始分类统计各运单状态...");
+        CapacityCounts counts = aggByWaybill.entrySet().stream()
+                .map(e -> classify(e.getKey(), e.getValue(), hasUnloadWaybillIds, leaveTimeByWaybill, waybillUpdate, twoHoursAgo))
+                .reduce(CapacityCounts.ZERO, CapacityCounts::add);
+
+        log.info("=== 运力分析完成 === - 待入场: {}, 装卸作业中: {}, 配送中: {}",
+                counts.pending(), counts.loading(), counts.delivering());
+
+        vo.setPendingEntryCount(counts.pending());
+        vo.setLoadingOperationCount(counts.loading());
+        vo.setDeliveringCount(counts.delivering());
+        return vo;
+    }
+
+    /**
+     * 部分聚合:将单个子任务转换为聚合对象
+     * <p>
+     * 逻辑:
+     * 1. 记录当前子任务的状态。
+     * 2. 如果状态处于“出场后、卸货前”窗口期,记录其更新时间,用于后续判断活跃性。
+     *
+     * @param st 子任务对象
+     * @return 聚合对象
+     */
+    private static WaybillSubtaskAgg partialAgg(KwtWaybillOrderSubtask st) {
+        WaybillSubtaskAgg a = new WaybillSubtaskAgg();
+        a.maxStatus = st.getStatus();
+        // 如果状态处于“出场后、卸货前”窗口期,记录其更新时间
+        if (inPostExitPreUnloadWindow(st.getStatus()) && st.getUpdateTime() != null) {
+            a.lastUpdateWhenPostExit = st.getUpdateTime();
+        }
+        return a;
+    }
+
+    /**
+     * 合并聚合:合并两个聚合对象
+     * <p>
+     * 逻辑:
+     * 1. 取两个对象中较大的状态值作为最终状态。
+     * 2. 取“出场后、卸货前”窗口期的最晚更新时间。
+     *
+     * @param a 聚合对象A
+     * @param b 聚合对象B
+     * @return 合并后的聚合对象
+     */
+    private static WaybillSubtaskAgg mergeAgg(WaybillSubtaskAgg a, WaybillSubtaskAgg b) {
+        WaybillSubtaskAgg r = new WaybillSubtaskAgg();
+        // 取最大状态值,代表该运单达到的最高进度
+        r.maxStatus = Math.max(a.maxStatus, b.maxStatus);
+        // 取“出场后、卸货前”窗口期的最晚更新时间
+        r.lastUpdateWhenPostExit = Stream.of(a.lastUpdateWhenPostExit, b.lastUpdateWhenPostExit)
+                .filter(Objects::nonNull)
+                .max(Date::compareTo)
+                .orElse(null);
+        return r;
+    }
+
+    /**
+     * 判断状态是否处于“出场后、卸货前”窗口期
+     * <p>
+     * 定义范围:WAIT_LOADING <= status < COMPLETION_LOADING
+     * 此区间通常对应车辆已出场但在途或等待卸货的状态。
+     *
+     * @param status 状态码
+     * @return 是否在窗口期内
+     */
+    private static boolean inPostExitPreUnloadWindow(int status) {
+        return status >= CarWaybillV1Enum.WAIT_LOADING.getCode()
+                && status < CarWaybillV1Enum.COMPLETION_LOADING.getCode();
+    }
+
+    /**
+     * 分类单个运单的状态
+     * <p>
+     * 优先级:
+     * 1. 无效/驳回状态 -> 忽略
+     * 2. 配送中 (Delivering)
+     * 3. 待入场 (Pending Entry)
+     * 4. 装卸作业中 (Loading Operation)
+     * 5. 其他 -> 忽略
+     *
+     * @param wid                 运单ID
+     * @param agg                 运单子任务聚合信息
+     * @param hasUnloadWaybillIds 包含卸货地址的运单ID集合
+     * @param leaveTimeByWaybill  门岗离场时间映射
+     * @param waybillUpdate       运单主表更新时间映射
+     * @param twoHoursAgo         两小时前的时间点
+     * @return 分类计数结果
+     */
+    private static CapacityCounts classify(
+            Long wid,
+            WaybillSubtaskAgg agg,
+            Set<Long> hasUnloadWaybillIds,
+            Map<Long, Date> leaveTimeByWaybill,
+            Map<Long, Date> waybillUpdate,
+            Date twoHoursAgo) {
+        
+        int ms = agg.maxStatus;
+        
+        // 无效状态或审核驳回,不计入任何类别
+        if (ms <= 0 || Objects.equals(ms, CarWaybillV1Enum.REVIEW_REJECTION.getCode())) {
+            log.trace("运单ID: {} 状态无效或已驳回 (status={}), 忽略", wid, ms);
+            return CapacityCounts.ZERO;
+        }
+
+        boolean hasUnload = hasUnloadWaybillIds.contains(wid);
+        
+        // 优先判断是否为“配送中”
+        if (isDelivering(ms, hasUnload, leaveTimeByWaybill.get(wid), agg.lastUpdateWhenPostExit, waybillUpdate.get(wid), twoHoursAgo)) {
+            log.trace("运单ID: {} 分类为: 配送中", wid);
+            return CapacityCounts.deliveringOne();
+        }
+        
+        // 判断是否为“待入场”
+        if (ms == CarWaybillV1Enum.PENDING_VEHICLE.getCode()) {
+            log.trace("运单ID: {} 分类为: 待入场", wid);
+            return CapacityCounts.pendingOne();
+        }
+        
+        // 判断是否为“装卸作业中”
+        // 包括:拒绝通行、出场完成、称重通行
+        if (ms == CarWaybillV1Enum.REFUSE_TRAFFIC.getCode()
+                || ms == CarWaybillV1Enum.EXIT_COMPLETED.getCode()
+                || ms == CarWaybillV1Enum.WEIGHT_TRAFFIC.getCode()) {
+            log.trace("运单ID: {} 分类为: 装卸作业中 (status={})", wid, ms);
+            return CapacityCounts.loadingOne();
+        }
+        
+        // 其他状态不计入
+        log.trace("运单ID: {} 状态未匹配任何分类 (status={}), 忽略", wid, ms);
+        return CapacityCounts.ZERO;
+    }
+
+    /**
+     * 判断运单是否处于“配送中”状态
+     * <p>
+     * 逻辑:
+     * 1. 排除审核驳回和已取消状态
+     * 2. 如果有卸货地址:状态在 [WAIT_LOADING, COMPLETION_LOADING) 之间视为配送中
+     * 3. 如果没有卸货地址:
+     *    - 状态必须在 [WAIT_LOADING, COMPLETION_LOADING) 之间
+     *    - 且参考时间(门岗离场时间 > 子任务更新时间 > 运单更新时间)必须在两小时内
+     *
+     * @param ms                    最大状态码
+     * @param hasUnload             是否有卸货地址
+     * @param gateLeave             门岗离场时间
+     * @param lastPostExitSubtaskUpd 子任务在出场后卸货前的最后更新时间
+     * @param waybillUpdate         运单主表更新时间
+     * @param twoHoursAgo           两小时前的时间点
+     * @return 是否配送中
+     */
+    private static boolean isDelivering(int ms, boolean hasUnload, Date gateLeave, Date lastPostExitSubtaskUpd,
+                                        Date waybillUpdate, Date twoHoursAgo) {
+        // 排除终止状态
+        if (Objects.equals(ms, CarWaybillV1Enum.REVIEW_REJECTION.getCode())
+                || Objects.equals(ms, CarWaybillV1Enum.CANCELLED.getCode())) {
+            return false;
+        }
+
+        if (hasUnload) {
+            // 有卸货地址:只要状态在等待装货到完成装货之间,即视为在途/配送
+            // 因为存在明确的卸货点,状态在此区间即代表正在执行运输任务
+            boolean isDelivering = ms >= CarWaybillV1Enum.WAIT_LOADING.getCode()
+                    && ms < CarWaybillV1Enum.COMPLETION_LOADING.getCode();
+            if (isDelivering) {
+                log.trace("运单有卸货地址且状态在配送区间, 判定为配送中");
+            }
+            return isDelivering;
+        }
+
+        // 无卸货地址:必须满足状态区间且时间在两小时内
+        // 防止历史数据或长时间停滞的数据被误判为配送中
+        if (ms < CarWaybillV1Enum.WAIT_LOADING.getCode()
+                || ms >= CarWaybillV1Enum.COMPLETION_LOADING.getCode()) {
+            return false;
+        }
+
+        // 确定参考时间:优先使用门岗离场时间,其次子任务更新时间,最后运单更新时间
+        Date ref = Optional.ofNullable(gateLeave)
+                .or(() -> Optional.ofNullable(lastPostExitSubtaskUpd))
+                .or(() -> Optional.ofNullable(waybillUpdate))
+                .orElse(null);
+        
+        // 参考时间存在且未超过两小时
+        boolean isActive = ref != null && !ref.before(twoHoursAgo);
+        if (isActive) {
+            log.trace("运单无卸货地址,但状态在配送区间且最近活跃 (ref={}), 判定为配送中", ref);
+        } else {
+            log.trace("运单无卸货地址,虽状态在配送区间但不活跃 (ref={}), 不判定为配送中", ref);
+        }
+        return isActive;
+    }
+
+    /**
+     * 加载包含卸货地址的运单ID集合
+     *
+     * @param wOrderIds 运单ID集合
+     * @return 包含卸货地址的运单ID集合
+     */
+    private Set<Long> loadUnloadWaybillIds(Set<Long> wOrderIds) {
+        if (CollectionUtils.isEmpty(wOrderIds)) {
+            return Set.of();
+        }
+        log.debug("正在查询 {} 个运单的地址信息以筛选卸货地址...", wOrderIds.size());
+        // addressType = 2 表示卸货地址
+        Set<Long> result = waybillOrderAddressRepository.queryBywOrderIds(new ArrayList<>(wOrderIds)).stream()
+                .filter(a -> Objects.equals(NumberConstant.TWO, a.getAddressType()))
+                .map(KwtWaybillOrderAddress::getWOrderId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        log.debug("筛选出 {} 个包含卸货地址的运单", result.size());
+        return result;
+    }
+
+    /**
+     * 加载门岗离场时间
+     *
+     * @param wOrderIds 运单ID集合
+     * @return 运单ID -> 离场时间的映射
+     */
+    private Map<Long, Date> loadGatekeeperLeaveTime(Set<Long> wOrderIds) {
+        if (CollectionUtils.isEmpty(wOrderIds)) {
+            return Map.of();
+        }
+        log.debug("正在查询 {} 个运单的门岗记录...", wOrderIds.size());
+        List<KwtGatekeeperWaybillOrder> rows = gatekeeperWaybillOrderRepository.queryGatekeeperWaybillOrderByWOrderIds(new ArrayList<>(wOrderIds));
+        if (CollectionUtils.isEmpty(rows)) {
+            log.debug("未查询到任何门岗记录");
+            return Map.of();
+        }
+        
+        // 按运单ID分组,并提取每个运单最新的已离场记录的时间
+        Map<Long, Date> result = rows.stream()
+                .collect(Collectors.groupingBy(KwtGatekeeperWaybillOrder::getWOrderId))
+                .entrySet().stream()
+                .map(SubtaskCapacityAnalysisService::latestExitedLeaveEntry)
+                .flatMap(Optional::stream)
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        
+        log.debug("解析出 {} 个运单的有效离场时间", result.size());
+        return result;
+    }
+
+    /**
+     * 获取单个运单最新的已离场记录的离场时间
+     *
+     * @param e 运单ID及其门岗记录列表
+     * @return 运单ID -> 离场时间的可选映射
+     */
+    private static Optional<Map.Entry<Long, Date>> latestExitedLeaveEntry(Map.Entry<Long, List<KwtGatekeeperWaybillOrder>> e) {
+        Long wid = e.getKey();
+        return e.getValue().stream()
+                // 取ID最大的记录(通常代表最新)
+                .max(Comparator.comparing(KwtGatekeeperWaybillOrder::getId, Comparator.nullsLast(Long::compareTo)))
+                // 过滤出状态为“已出场”或“空车出场”的记录
+                .filter(gk -> GatekeeperStatusEnum.EXITED.getCode().equals(gk.getStatus())
+                        || GatekeeperStatusEnum.EMPTY_EXITED.getCode().equals(gk.getStatus()))
+                // 取离场时间,若为空则取更新时间
+                .map(gk -> Optional.ofNullable(gk.getLeaveTime()).orElse(gk.getUpdateTime()))
+                .filter(Objects::nonNull)
+                .map(leave -> new AbstractMap.SimpleEntry<>(wid, leave));
+    }
+
+    /**
+     * 加载运单主表的更新时间
+     *
+     * @param wOrderIds 运单ID集合
+     * @return 运单ID -> 更新时间的映射
+     */
+    private Map<Long, Date> loadWaybillUpdateTime(Set<Long> wOrderIds) {
+        if (CollectionUtils.isEmpty(wOrderIds)) {
+            return Map.of();
+        }
+        log.debug("正在查询 {} 个运单的主表更新时间...", wOrderIds.size());
+        Map<Long, Date> result = waybillOrderRepository.listByIds(wOrderIds).stream()
+                .filter(wo -> wo.getId() != null && wo.getUpdateTime() != null)
+                .collect(Collectors.toMap(KwtWaybillOrder::getId, KwtWaybillOrder::getUpdateTime));
+        log.debug("获取到 {} 个运单的主表更新时间", result.size());
+        return result;
+    }
+
+    /**
+     * 容量计数记录
+     * <p>
+     * 用于累加统计三种状态的运单数量
+     */
+    private record CapacityCounts(int pending, int loading, int delivering) {
+        static final CapacityCounts ZERO = new CapacityCounts(0, 0, 0);
+
+        static CapacityCounts pendingOne() {
+            return new CapacityCounts(1, 0, 0);
+        }
+
+        static CapacityCounts loadingOne() {
+            return new CapacityCounts(0, 1, 0);
+        }
+
+        static CapacityCounts deliveringOne() {
+            return new CapacityCounts(0, 0, 1);
+        }
+
+        CapacityCounts add(CapacityCounts o) {
+            return new CapacityCounts(pending + o.pending, loading + o.loading, delivering + o.delivering);
+        }
+    }
+
+    /**
+     * 运单子任务聚合信息
+     * <p>
+     * 用于在 Stream 归约过程中暂存每个运单的关键状态信息
+     */
+    private static final class WaybillSubtaskAgg {
+        /**
+         * 最大状态码
+         * <p>
+         * 代表该运单下所有子任务中达到的最高业务状态
+         */
+        private int maxStatus;
+        /**
+         * 在“出场后、卸货前”窗口期的最后更新时间
+         * <p>
+         * 用于辅助判断无卸货地址运单的活跃性
+         */
+        private Date lastUpdateWhenPostExit;
+    }
+}

+ 65 - 0
sckw-modules/sckw-transport/src/test/java/com/sckw/transport/service/dashboard/RealtimeSalesVolumeServiceTest.java

@@ -0,0 +1,65 @@
+package com.sckw.transport.service.dashboard;
+
+import com.sckw.core.common.enums.enums.DictEnum;
+import com.sckw.transport.model.KwtWaybillOrderSubtask;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+
+/**
+ * 实时销量服务单元测试。
+ */
+public class RealtimeSalesVolumeServiceTest {
+
+    /**
+     * 按装货量计费时,应取子任务装货量作为净重。
+     */
+    @Test
+    public void subtaskNetTonShouldUseLoadAmountWhenBillingByLoadAmount() {
+        KwtWaybillOrderSubtask subtask = buildSubtask("10.50", "9.25");
+
+        BigDecimal actual = RealtimeSalesVolumeService.subtaskNetTon(subtask, DictEnum.CHARGING_TYPE_1.getValue());
+
+        Assert.assertEquals(new BigDecimal("10.50"), actual);
+    }
+
+    /**
+     * 按卸货量计费时,应取子任务卸货量作为净重。
+     */
+    @Test
+    public void subtaskNetTonShouldUseUnloadAmountWhenBillingByUnloadAmount() {
+        KwtWaybillOrderSubtask subtask = buildSubtask("10.50", "9.25");
+
+        BigDecimal actual = RealtimeSalesVolumeService.subtaskNetTon(subtask, DictEnum.CHARGING_TYPE_2.getValue());
+
+        Assert.assertEquals(new BigDecimal("9.25"), actual);
+    }
+
+    /**
+     * 计费方式缺失时保持默认卸货量口径,空值或非正数返回0。
+     */
+    @Test
+    public void subtaskNetTonShouldDefaultToUnloadAmountAndIgnoreInvalidAmount() {
+        KwtWaybillOrderSubtask subtask = buildSubtask("10.50", "-1.00");
+
+        BigDecimal actual = RealtimeSalesVolumeService.subtaskNetTon(subtask, null);
+
+        Assert.assertEquals(BigDecimal.ZERO, actual);
+        Assert.assertEquals(BigDecimal.ZERO, RealtimeSalesVolumeService.subtaskNetTon(null, DictEnum.CHARGING_TYPE_1.getValue()));
+    }
+
+    /**
+     * 构造子任务测试数据。
+     *
+     * @param loadAmount   装货量
+     * @param unloadAmount 卸货量
+     * @return 子任务对象
+     */
+    private KwtWaybillOrderSubtask buildSubtask(String loadAmount, String unloadAmount) {
+        KwtWaybillOrderSubtask subtask = new KwtWaybillOrderSubtask();
+        subtask.setLoadAmount(new BigDecimal(loadAmount));
+        subtask.setUnloadAmount(new BigDecimal(unloadAmount));
+        return subtask;
+    }
+}