Explorar o código

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

donglang hai 1 semana
pai
achega
74fe9da9bc

+ 170 - 4
sckw-modules/sckw-transport/src/main/java/com/sckw/transport/service/dashboard/RealtimeSalesVolumeService.java

@@ -2,9 +2,13 @@ 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.common.enums.enums.DictTypeEnum;
 import com.sckw.core.model.constant.Global;
 import com.sckw.core.model.enums.CarWaybillV1Enum;
 import com.sckw.core.utils.CollectionUtils;
+import com.sckw.product.api.dubbo.GoodsInfoService;
+import com.sckw.product.api.model.KwpGoods;
+import com.sckw.system.api.RemoteSystemService;
 import com.sckw.transport.api.model.vo.RealtimeSalesGoodsSeriesVo;
 import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
 import com.sckw.transport.model.KwtLogisticsOrder;
@@ -19,6 +23,7 @@ import com.sckw.transport.repository.KwtWaybillOrderRepository;
 import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -28,6 +33,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
@@ -85,6 +91,12 @@ public class RealtimeSalesVolumeService {
     private final KwtLogisticsOrderRepository logisticsOrderRepository;
     private final KwtLogisticsOrderUnitRepository logisticsOrderUnitRepository;
 
+    @DubboReference(version = "1.0.0", group = "design", check = false, timeout = 6000)
+    private GoodsInfoService goodsInfoService;
+
+    @DubboReference(version = "1.0.0", group = "design", check = false, timeout = 6000)
+    private RemoteSystemService remoteSystemService;
+
     /**
      * 构建实时销量视图对象
      *
@@ -157,11 +169,14 @@ public class RealtimeSalesVolumeService {
         Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder = pickPrimaryGoods(
                 logisticsOrderGoodsRepository.queryByLogOrderIds(new ArrayList<>(lOrderIds)));
         log.debug("匹配到 {} 个物流订单的商品信息", goodsByLogOrder.size());
+        Map<Long, KwpGoods> productGoodsById = queryProductGoodsById(goodsByLogOrder);
+        Map<String, String> goodsSpecMap = queryGoodsSpecMap();
 
         // 8. 处理每个运单,将其贡献累加到对应的商品和时间槽中
         Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap = new HashMap<>();
         waybills.stream()
-                .flatMap(wo -> contributionStream(wo, windowStart, windowEnd, slotIndexByEnd, netByWaybill, goodsByLogOrder))
+                .flatMap(wo -> contributionStream(wo, windowStart, windowEnd, slotIndexByEnd, netByWaybill,
+                        goodsByLogOrder, productGoodsById, goodsSpecMap))
                 .forEach(c -> c.accumulate(seriesMap));
 
         // 9. 将结果转换为列表并按商品名称排序
@@ -209,7 +224,9 @@ public class RealtimeSalesVolumeService {
             Date windowEnd,
             Map<ZonedDateTime, Integer> slotIndexByEnd,
             Map<Long, BigDecimal> netByWaybill,
-            Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder) {
+            Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder,
+            Map<Long, KwpGoods> productGoodsById,
+            Map<String, String> goodsSpecMap) {
 
         // 获取运单的实际完成时间
         Date completedAt = completionInstant(wo);
@@ -237,10 +254,11 @@ public class RealtimeSalesVolumeService {
                 .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);
+                    String goodsDisplayName = buildGoodsDisplayName(g, productGoodsById, goodsSpecMap);
+                    log.trace("运单 ID: {} 贡献商品: {}, 重量: {} 吨, 时间槽索引: {}", wo.getId(), goodsDisplayName, tons, slotIdx);
                     return Stream.of(new WaybillContribution(
                             g.getGoodsId(),
-                            g.getGoodsName(),
+                            goodsDisplayName,
                             slotIdx,
                             tons));
                 })
@@ -277,6 +295,154 @@ public class RealtimeSalesVolumeService {
         return result;
     }
 
+    /**
+     * 批量查询商品主数据,用于将实时销量图例展示为“商品名称/规格”。
+     * <p>
+     * 该方法从物流订单关联的商品ID中提取唯一ID列表,并通过 Dubbo 服务远程获取商品详细信息(如标准名称、规格代码等)。
+     * 若远程调用失败或返回空,则记录错误日志并返回空映射,确保主流程不因非核心数据缺失而中断。
+     *
+     * @param goodsByLogOrder 物流订单ID到主要商品明细的映射,作为提取商品ID的数据源
+     * @return 商品ID (Long) -> 商品主数据对象 (KwpGoods) 的映射;若输入为空或查询异常则返回空映射
+     */
+    private Map<Long, KwpGoods> queryProductGoodsById(Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder) {
+        // 1. 参数校验:如果输入映射为空或null,直接返回空映射,避免无效处理
+        if (goodsByLogOrder == null || goodsByLogOrder.isEmpty()) {
+            log.debug("输入的商品映射为空,跳过商品主数据查询。");
+            return Collections.emptyMap();
+        }
+
+        // 2. 提取并去重商品ID
+        List<Long> goodsIds = goodsByLogOrder.values().stream()
+                .filter(Objects::nonNull)          // 过滤掉值为null的商品对象
+                .map(KwtLogisticsOrderGoods::getGoodsId) // 提取商品ID
+                .filter(Objects::nonNull)          // 过滤掉ID为null的记录
+                .distinct()                        // 去重,减少远程调用压力
+                .collect(Collectors.toCollection(ArrayList::new)); // 收集为List以支持后续操作
+
+        // 3. 再次校验:如果没有有效的商品ID,直接返回空映射
+        if (CollectionUtils.isEmpty(goodsIds)) {
+            log.debug("未提取到有效的商品ID,跳过商品主数据查询。");
+            return Collections.emptyMap();
+        }
+        
+        log.debug("准备查询 {} 个商品的主数据信息,goodsIds={}", goodsIds.size(), goodsIds);
+
+        // 4. 远程调用获取商品详情
+        try {
+            Map<Long, KwpGoods> result = goodsInfoService.getGoodsByIds(goodsIds);
+            
+            // 5. 处理返回结果:防止远程服务返回null导致NPE
+            if (result == null) {
+                log.warn("远程服务返回的商品主数据为null,goodsIds={}", goodsIds);
+                return Collections.emptyMap();
+            }
+            
+            log.debug("成功查询到 {} 个商品的主数据信息。", result.size());
+            return result;
+            
+        } catch (Exception e) {
+            // 6. 异常处理:记录详细错误日志,降级返回空映射,保证看板数据展示不崩溃
+            log.error("查询商品主数据发生异常,可能导致部分商品名称显示不完整。goodsIds={}", goodsIds, e);
+            return Collections.emptyMap();
+        }
+    }
+
+
+    /**
+     * 查询商品规格字典映射
+     * <p>
+     * 从远程系统服务获取商品规格(GOODS_SPEC)的字典数据,用于将规格代码转换为展示名称。
+     *
+     * @return 规格代码 -> 展示名称的映射;若查询失败或无数据则返回空映射
+     */
+    private Map<String, String> queryGoodsSpecMap() {
+        String dictType = DictTypeEnum.GOODS_SPEC.getType();
+        log.debug("开始查询商品规格字典,dictType={}", dictType);
+        try {
+            // 调用远程系统服务查询指定类型的字典数据
+            Map<String, Map<String, String>> dictMap = remoteSystemService.queryDictByType(List.of(dictType));
+            
+            // 校验返回结果是否为空
+            if (dictMap == null || dictMap.isEmpty()) {
+                log.warn("远程系统返回的商品规格字典为空,dictType={}", dictType);
+                return Collections.emptyMap();
+            }
+            
+            // 提取具体类型的字典映射
+            Map<String, String> goodsSpecMap = dictMap.get(dictType);
+            if (goodsSpecMap == null || goodsSpecMap.isEmpty()) {
+                log.debug("未找到类型为 {} 的具体字典项,返回空映射。", dictType);
+                return Collections.emptyMap();
+            }
+            
+            log.debug("成功查询商品规格字典,共 {} 条记录。", goodsSpecMap.size());
+            return goodsSpecMap;
+        } catch (Exception e) {
+            // 捕获异常,记录错误日志,避免影响主流程,降级返回空映射
+            log.error("查询商品规格字典发生异常,dictType={}", dictType, e);
+            return Collections.emptyMap();
+        }
+    }
+
+    /**
+     * 构建商品展示名称:优先使用 kwp_goods.name,并追加 goods_spec 字典中的规格名称。
+     * <p>
+     * 命名规则:
+     * 1. 优先从商品主数据 (KwpGoods) 获取标准名称。
+     * 2. 若主数据名称缺失,则降级使用物流订单货物明细中的名称。
+     * 3. 若存在有效的规格代码 (spec),尝试从字典中解析为可读的规格名称。
+     * 4. 最终格式为 "商品名称/规格名称";若规格缺失或无效,则仅返回商品名称。
+     *
+     * @param orderGoods       物流订单商品明细,作为基础数据来源
+     * @param productGoodsById 商品ID到商品主数据的映射,用于获取标准化信息
+     * @param goodsSpecMap     商品规格字典映射 (key: spec代码, value: 展示名称)
+     * @return 格式化后的商品展示名称,若输入为空则返回空字符串
+     */
+    static String buildGoodsDisplayName(KwtLogisticsOrderGoods orderGoods,
+                                        Map<Long, KwpGoods> productGoodsById,
+                                        Map<String, String> goodsSpecMap) {
+        // 防御性检查:如果订单货物对象为空,直接返回空字符串
+        if (orderGoods == null) {
+            return "";
+        }
+
+        // 1. 获取商品主数据对象
+        Long goodsId = orderGoods.getGoodsId();
+        KwpGoods productGoods = Optional.ofNullable(productGoodsById)
+                .map(map -> map.get(goodsId))
+                .orElse(null);
+
+        // 2. 确定商品名称:优先使用主数据名称,其次使用订单明细名称
+        String goodsName = Optional.ofNullable(productGoods)
+                .map(KwpGoods::getName)
+                .filter(name -> !name.isBlank())
+                .orElse(Optional.ofNullable(orderGoods.getGoodsName()).orElse(""));
+
+        // 3. 确定规格名称:从主数据获取规格代码,并在字典中查找对应的展示名称
+        String specValue = Optional.ofNullable(productGoods)
+                .map(KwpGoods::getSpec)
+                .orElse(null);
+        
+        String specName = "";
+        if (specValue != null) {
+            specName = Optional.ofNullable(goodsSpecMap)
+                    .map(map -> map.get(specValue))
+                    .filter(spec -> !spec.isBlank())
+                    .orElse("");
+        }
+
+        // 4. 组装最终展示名称
+        // 如果商品名称为空,或者规格名称为空,则不添加斜杠分隔符
+        if (goodsName.isBlank() || specName.isBlank()) {
+            log.trace("构建商品展示名称: goodsId={}, 结果='{}' (规格名为空或商品名为空)", goodsId, goodsName);
+            return goodsName;
+        }
+
+        String finalName = goodsName + "/" + specName;
+        log.trace("构建商品展示名称: goodsId={}, 结果='{}'", goodsId, finalName);
+        return finalName;
+    }
+
     /**
      * 查询物流订单计费方式映射。
      *

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

@@ -1,11 +1,14 @@
 package com.sckw.transport.service.dashboard;
 
 import com.sckw.core.common.enums.enums.DictEnum;
+import com.sckw.product.api.model.KwpGoods;
+import com.sckw.transport.model.KwtLogisticsOrderGoods;
 import com.sckw.transport.model.KwtWaybillOrderSubtask;
 import org.junit.Assert;
 import org.junit.Test;
 
 import java.math.BigDecimal;
+import java.util.Map;
 
 /**
  * 实时销量服务单元测试。
@@ -49,6 +52,39 @@ public class RealtimeSalesVolumeServiceTest {
         Assert.assertEquals(BigDecimal.ZERO, RealtimeSalesVolumeService.subtaskNetTon(null, DictEnum.CHARGING_TYPE_1.getValue()));
     }
 
+    /**
+     * 商品主数据存在规格时,实时销量商品名称应拼接规格字典展示值。
+     */
+    @Test
+    public void buildGoodsDisplayNameShouldAppendSpecLabel() {
+        KwtLogisticsOrderGoods orderGoods = buildOrderGoods(100L, "石灰石");
+        KwpGoods productGoods = new KwpGoods()
+                .setName("石灰石")
+                .setSpec("S1");
+
+        String actual = RealtimeSalesVolumeService.buildGoodsDisplayName(
+                orderGoods,
+                Map.of(100L, productGoods),
+                Map.of("S1", "10-20mm"));
+
+        Assert.assertEquals("石灰石/10-20mm", actual);
+    }
+
+    /**
+     * 商品主数据或规格字典缺失时,展示名称回退为物流订单商品名称。
+     */
+    @Test
+    public void buildGoodsDisplayNameShouldFallbackToOrderGoodsNameWhenSpecMissing() {
+        KwtLogisticsOrderGoods orderGoods = buildOrderGoods(100L, "石灰石");
+
+        String actual = RealtimeSalesVolumeService.buildGoodsDisplayName(
+                orderGoods,
+                Map.of(),
+                Map.of());
+
+        Assert.assertEquals("石灰石", actual);
+    }
+
     /**
      * 构造子任务测试数据。
      *
@@ -62,4 +98,18 @@ public class RealtimeSalesVolumeServiceTest {
         subtask.setUnloadAmount(new BigDecimal(unloadAmount));
         return subtask;
     }
+
+    /**
+     * 构造物流订单商品测试数据。
+     *
+     * @param goodsId   商品ID
+     * @param goodsName 商品名称
+     * @return 物流订单商品对象
+     */
+    private KwtLogisticsOrderGoods buildOrderGoods(Long goodsId, String goodsName) {
+        KwtLogisticsOrderGoods orderGoods = new KwtLogisticsOrderGoods();
+        orderGoods.setGoodsId(goodsId);
+        orderGoods.setGoodsName(goodsName);
+        return orderGoods;
+    }
 }