|
@@ -2,9 +2,13 @@ package com.sckw.transport.service.dashboard;
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import com.sckw.core.common.enums.enums.DictEnum;
|
|
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.constant.Global;
|
|
|
import com.sckw.core.model.enums.CarWaybillV1Enum;
|
|
import com.sckw.core.model.enums.CarWaybillV1Enum;
|
|
|
import com.sckw.core.utils.CollectionUtils;
|
|
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.RealtimeSalesGoodsSeriesVo;
|
|
|
import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
|
|
import com.sckw.transport.api.model.vo.RealtimeSalesVolumeVo;
|
|
|
import com.sckw.transport.model.KwtLogisticsOrder;
|
|
import com.sckw.transport.model.KwtLogisticsOrder;
|
|
@@ -19,6 +23,7 @@ import com.sckw.transport.repository.KwtWaybillOrderRepository;
|
|
|
import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
|
|
import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.apache.dubbo.config.annotation.DubboReference;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.BigDecimal;
|
|
@@ -28,6 +33,7 @@ import java.time.ZonedDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
import java.time.temporal.ChronoUnit;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
import java.util.Comparator;
|
|
import java.util.Comparator;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
@@ -85,6 +91,12 @@ public class RealtimeSalesVolumeService {
|
|
|
private final KwtLogisticsOrderRepository logisticsOrderRepository;
|
|
private final KwtLogisticsOrderRepository logisticsOrderRepository;
|
|
|
private final KwtLogisticsOrderUnitRepository logisticsOrderUnitRepository;
|
|
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(
|
|
Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder = pickPrimaryGoods(
|
|
|
logisticsOrderGoodsRepository.queryByLogOrderIds(new ArrayList<>(lOrderIds)));
|
|
logisticsOrderGoodsRepository.queryByLogOrderIds(new ArrayList<>(lOrderIds)));
|
|
|
log.debug("匹配到 {} 个物流订单的商品信息", goodsByLogOrder.size());
|
|
log.debug("匹配到 {} 个物流订单的商品信息", goodsByLogOrder.size());
|
|
|
|
|
+ Map<Long, KwpGoods> productGoodsById = queryProductGoodsById(goodsByLogOrder);
|
|
|
|
|
+ Map<String, String> goodsSpecMap = queryGoodsSpecMap();
|
|
|
|
|
|
|
|
// 8. 处理每个运单,将其贡献累加到对应的商品和时间槽中
|
|
// 8. 处理每个运单,将其贡献累加到对应的商品和时间槽中
|
|
|
Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap = new HashMap<>();
|
|
Map<Long, RealtimeSalesGoodsSeriesVo> seriesMap = new HashMap<>();
|
|
|
waybills.stream()
|
|
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));
|
|
.forEach(c -> c.accumulate(seriesMap));
|
|
|
|
|
|
|
|
// 9. 将结果转换为列表并按商品名称排序
|
|
// 9. 将结果转换为列表并按商品名称排序
|
|
@@ -209,7 +224,9 @@ public class RealtimeSalesVolumeService {
|
|
|
Date windowEnd,
|
|
Date windowEnd,
|
|
|
Map<ZonedDateTime, Integer> slotIndexByEnd,
|
|
Map<ZonedDateTime, Integer> slotIndexByEnd,
|
|
|
Map<Long, BigDecimal> netByWaybill,
|
|
Map<Long, BigDecimal> netByWaybill,
|
|
|
- Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder) {
|
|
|
|
|
|
|
+ Map<Long, KwtLogisticsOrderGoods> goodsByLogOrder,
|
|
|
|
|
+ Map<Long, KwpGoods> productGoodsById,
|
|
|
|
|
+ Map<String, String> goodsSpecMap) {
|
|
|
|
|
|
|
|
// 获取运单的实际完成时间
|
|
// 获取运单的实际完成时间
|
|
|
Date completedAt = completionInstant(wo);
|
|
Date completedAt = completionInstant(wo);
|
|
@@ -237,10 +254,11 @@ public class RealtimeSalesVolumeService {
|
|
|
.filter(g -> g.getGoodsId() != null) // 确保商品ID有效
|
|
.filter(g -> g.getGoodsId() != null) // 确保商品ID有效
|
|
|
.map(g -> {
|
|
.map(g -> {
|
|
|
BigDecimal tons = netByWaybill.getOrDefault(wo.getId(), BigDecimal.ZERO);
|
|
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(
|
|
return Stream.of(new WaybillContribution(
|
|
|
g.getGoodsId(),
|
|
g.getGoodsId(),
|
|
|
- g.getGoodsName(),
|
|
|
|
|
|
|
+ goodsDisplayName,
|
|
|
slotIdx,
|
|
slotIdx,
|
|
|
tons));
|
|
tons));
|
|
|
})
|
|
})
|
|
@@ -277,6 +295,154 @@ public class RealtimeSalesVolumeService {
|
|
|
return result;
|
|
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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 查询物流订单计费方式映射。
|
|
* 查询物流订单计费方式映射。
|
|
|
*
|
|
*
|