构建高级动态报表:从流水到可搜索、分页、动态列序的产品渠道库存视图!!!

🚀 构建高级动态报表:从流水到可搜索、分页、动态列序的产品渠道库存视图 📊

各位数据魔法师和后端架构师们,大家好!👋 在现代企业应用中,能够快速、灵活地从海量流水数据中提取有价值的洞察至关重要。今天,我们将挑战一个更高级的报表需求:构建一个“产品渠道库存视图”,它不仅能对数据进行全局聚合,还能支持对聚合结果进行动态搜索分页,并且其渠道列能根据预设规则(如序号)动态排序

想象一下,运营经理希望看到:“特定管理员名下,所有产品(可按名称搜索,结果分页)在各个渠道(渠道按重要性排序)的当前库存和已付款数量分别是多少?” 这是一个典型的复杂报表场景,我们将用 Java、Spring Boot 和 Spring Data JPA 来攻克它!✨

🎯 报表目标:智能、有序、可交互的库存洞察

我们最终要呈现给用户的报表视图,应具备以下特点:

  1. 全局聚合:每个产品的数据(如各渠道库存、总寄售数量、总已付款数量)是基于该管理员名下所有相关流水计算得出的。
  2. 动态渠道列序:表格中的渠道列(例如“渠道A”、“渠道B”)会根据其在 ConsignmentSettlement 表中的 sequence 字段进行降序排列(序号大的在前)。
  3. 渠道内多指标:每个渠道列下,会展示两个子指标:“库存数”和“已付款数”。
  4. 对聚合结果的搜索:用户可以对聚合后的产品列表进行搜索(例如按产品名称、条形码)。
  5. 对聚合结果的分页:搜索后的产品列表可以分页展示。

核心计算逻辑:

  • 渠道库存数 (channelStocks): 针对每个产品,在特定渠道下,状态(status)为 1-6 时的 stockInCount 之和。
  • 渠道已付款数 (paidCount for channel): 针对每个产品,在特定渠道下,状态(status)为 6 (已付款) 时的 paidCount 之和。
  • 产品总寄售数量: 某个产品在所有渠道的“渠道库存数”之和(在DTO中动态计算)。
  • 产品总已付款数量: 某个产品在所有渠道的“渠道已付款数”之和(在DTO中动态计算)。
  • 产品基础信息(条形码、品名、箱规):从 Product 主数据表中获取。
  • 渠道名称及顺序:从 ConsignmentSettlement 表中获取,并根据 sequence 字段排序。

🛠️ 技术栈与核心组件

  • 后端:Java, Spring Boot, Spring Data JPA, Lombok
  • 核心实体
    • ConsignmentSummary:寄售流水总表(含 productId, consignmentSettlementId, status, stockInCount, paidCount)。
    • ConsignmentSettlement:寄售结算单表(含 id, channelName, sequence)。
    • Product:产品主数据表(含 id, jancode, name, carton)。
  • DTOs (Data Transfer Objects)
    • ChannelDataDto:封装单个渠道的库存数和已付款数。
    • ProductChannelStockDto:封装单个产品的聚合信息,其 channelData 字段是一个 Map<String, ChannelDataDto>
    • AggregatedViewResult:服务层返回给Controller的包装对象,包含聚合后的产品数据列表和有序的渠道表头列表。
  • 分页与搜索参数封装PageWithSearch (继承自 BasePage)。
  • Repositories: ConsignmentSummaryRepository, ConsignmentSettlementRepository, ProductRepository
  • Service: ConsignmentViewService (核心聚合与表头排序逻辑)。
  • Controller: ConsignmentViewController (API接口,编排,内存搜索与分页)。

📝 实现步骤概览:聚合为王,智能展示

步骤描述关键点
1定义DTOs (ChannelDataDto, ProductChannelStockDto, AggregatedViewResult)ProductChannelStockDto 内的 channelData Map 存储 ChannelDataDtoAggregatedViewResult 包装最终结果。
2Controller: 获取全局流水根据 adminId 调用 ConsignmentSummaryRepository.findAllByAdminId() 获取所有相关 ConsignmentSummary性能关键点!
3Service: 执行全局聚合与渠道排序ConsignmentViewService.generateProductChannelStockView()
- 批量获取 ProductConsignmentSettlement
- 根据 ConsignmentSettlement.sequence 生成有序的渠道表头列表 (orderedChannelHeaders)。
- 聚合数据到 List<ProductChannelStockDto>,每个 DTO 的 channelData Map 包含各渠道的 stockCountpaidCount
- 返回 AggregatedViewResult
4Controller: 内存搜索对返回的 AggregatedViewResult.getProductsData()(即完整的聚合产品列表),根据 PageWithSearch 中的搜索条件进行内存筛选。
5Controller: 内存分页对搜索后的产品列表,根据 PageWithSearch 中的分页参数进行内存分页。
6Controller: 准备并返回响应组装 BaseResult,包含当前页的产品数据、有序的渠道表头以及针对聚合后产品列表的分页信息。

🌊 数据处理流程图 (Mermaid)

搜索后
无搜索条件
开始 (输入: adminId, PageWithSearch params)
Controller: CSRepo.findAllByAdminId(adminId)
获取【所有】相关 ConsignmentSummary (CS_all)
CS_all 是否为空?
返回空分页结果
Service (ConsignmentViewService):
generateProductChannelStockView(CS_all)
执行【全局聚合】
Service内部:
1. 批量获取 Product 数据, 构建 Product Map
2. 批量获取 ConsignmentSettlement 数据 (CS_list)
Service内部:
3. 根据 CS_list 的 sequence 字段【降序排序】
生成 orderedChannelHeaders (List)
4. 构建 settlementIdToChannelNameMap
Service内部:
5. 遍历 CS_all, 聚合数据到
List (Agg_Products_all)
(每个 ProductChannelStockDto.channelData
Map 包含库存和已付款)
Service返回: AggregatedViewResult
(含 Agg_Products_all 和 orderedChannelHeaders)
Agg_Products_all 是否为空?
Controller: IF params.field 和 params.value 有效 THEN
在内存中【搜索】Agg_Products_all
得到 List (Agg_Products_searched)
Agg_Products_searched = Agg_Products_all
Agg_Products_searched 是否为空?
Controller: 根据 params.page 和 params.size
在内存中对 Agg_Products_searched 进行【分页】
得到当前页 List (Paged_Data)
Controller: 组装 BaseResult (含 Paged_Data,
orderedChannelHeaders, 分页信息)
结束 (返回最终视图数据)

⏳ 时序图:一次高级报表API请求 (Mermaid Sequence Diagram)

前端应用 ConsignmentViewController ConsignmentViewService ConsignmentSummaryRepository ProductRepository ConsignmentSettlementRepository GET /fully-aggregated-product-channel-stock-ordered?adminId=123&field=name&value=精华&page=0&size=10 findAllByAdminId(123) 返回 List<ConsignmentSummary> (allRelevantSummaries) generateProductChannelStockView(allRelevantSummaries) findAllById(productIds) 返回 List<Product> findAllById(settlementIds) 返回 List<ConsignmentSettlement> 1. 根据ConsignmentSettlement.sequence排序渠道,生成orderedChannelHeaders 2. 聚合数据到List<ProductChannelStockDto> (每个含Map<String, ChannelDataDto>) 返回 AggregatedViewResult (含productsData和orderedChannelHeaders) 从AggregatedViewResult获取allAggregatedProducts和orderedChannelHeaders 在内存中对allAggregatedProducts进行搜索 (基于 "name" like "精华") 对搜索结果进行内存分页 (page=0, size=10) 组装 BaseResult (含分页后的产品数据、orderedChannelHeaders、分页信息) 返回 JSON 响应 前端应用 ConsignmentViewController ConsignmentViewService ConsignmentSummaryRepository ProductRepository ConsignmentSettlementRepository

💡 关键代码片段解析

ProductChannelStockDto.java (包含 ChannelDataDto)

// ChannelDataDto.java
public class ChannelDataDto {
    private int stockCount = 0;
    private int paidCount = 0;
    // addStock, addPaid 方法
}

// ProductChannelStockDto.java
public class ProductChannelStockDto {
    private Integer productId;
    private String jancode; /*...*/
    // 修改:Map 的值类型变为 ChannelDataDto
    private Map<String, ChannelDataDto> channelData = new HashMap<>();
    // addStockToChannel, addPaidToChannel 方法
    // getTotalConsignmentQuantity, getTotalProductPaidCount (动态计算产品级总数)
}

新的 ChannelDataDto 使得我们可以在每个渠道下存储多个指标。

ConsignmentViewService.java - 排序与聚合

// ...
public AggregatedViewResult generateProductChannelStockView(List<ConsignmentSummary> summaries) {
    // ...
    List<String> orderedChannelHeaders = Collections.emptyList();
    if (!settlementIds.isEmpty()) {
        List<ConsignmentSettlement> settlements = consignmentSettlementRepository.findAllById(settlementIds);
        // 获取按 sequence 排序的唯一 channelName 列表
        orderedChannelHeaders = settlements.stream()
                .filter(s -> /* ...有效性判断... */ s.getSequence() != null)
                .sorted(Comparator.comparing(ConsignmentSettlement::getSequence, Comparator.nullsLast(Integer::compareTo)).reversed())
                .map(ConsignmentSettlement::getChannelName)
                .distinct()
                .collect(Collectors.toList());
        // ... 构建 settlementIdToChannelNameMap ...
    }
    // ...
    for (ConsignmentSummary summary : summaries) {
        // ... (获取或创建 ProductChannelStockDto dto) ...
        String channelName = settlementIdToChannelNameMap.get(summary.getConsignmentSettlementId());
        if (channelName != null) {
            // 聚合渠道库存
            if (/*...*/) dto.addStockToChannel(channelName, summary.getStockInCount());
            // 聚合渠道已付款
            if (/*...status == PAID_STATUS...*/) dto.addPaidToChannel(channelName, summary.getPaidCount());
        }
    }
    // ...
    return new AggregatedViewResult(new ArrayList<>(productViewMap.values()), orderedChannelHeaders);
}
// ...

Service 层现在负责生成有序的渠道表头,并聚合每个渠道下的多个指标。

ConsignmentViewController.java - 内存搜索与分页

// ...
public BaseResult getFullyAggregatedProductChannelStockOrdered(/*...*/) {
    // 1. 获取所有相关流水
    List<ConsignmentSummary> allRelevantSummaries = consignmentSummaryRepository.findAllByAdminId(adminId);
    // 2. 全局聚合
    AggregatedViewResult aggregationResult = consignmentViewService.generateProductChannelStockView(allRelevantSummaries);
    List<ProductChannelStockDto> allAggregatedProducts = aggregationResult.getProductsData();
    List<String> orderedChannelHeaders = aggregationResult.getOrderedChannelHeaders();
    // 3. 内存搜索
    List<ProductChannelStockDto> searchedAggregatedProducts = allAggregatedProducts;
    if (/*...有搜索条件...*/) {
        // searchedAggregatedProducts = allAggregatedProducts.stream().filter(...).collect(Collectors.toList());
    }
    // 4. 内存分页
    // ... (使用 PageRequest, subList, PageImpl) ...
    Page<ProductChannelStockDto> productPageResult = new PageImpl<>(/*...*/);
    // 5. 组装响应
    resultData.put("channelHeaders", orderedChannelHeaders); // 使用有序表头
    // ...
    return BaseResult.success("查询成功", resultData);
}
// ...

Controller 现在清晰地执行:获取全量 -> 聚合 -> 搜索 -> 分页 的流程。

📖 常见英文缩写解释

  • DTO: Data Transfer Object (数据传输对象)
  • JPA: Java Persistence API (Java持久化API)
  • ORM: Object-Relational Mapping (对象关系映射)
  • API: Application Programming Interface (应用程序编程接口)
  • SQL: Structured Query Language (结构化查询语言)
  • UI: User Interface (用户界面)
  • OLAP: Online Analytical Processing (联机分析处理)
  • BI: Business Intelligence (商业智能)

🎉 总结与挑战

我们构建了一个功能相当完善的动态报表后端服务!它能够处理全局数据聚合,并对聚合结果提供灵活的搜索和分页功能,同时还能保证渠道列的动态有序展示。

核心亮点:

  • 关注点分离:Service 负责复杂聚合和业务规则,Controller 负责流程编排和与前端的交互。
  • 数据准确性:聚合基于全量相关数据。
  • 灵活性:支持对最终结果的搜索和分页,渠道列动态排序。

主要挑战与性能考量:

  • 初始数据加载consignmentSummaryRepository.findAllByAdminId(adminId) 必须高效。如果数据量过大,这里是首要优化点。
  • 内存消耗:加载所有流水和聚合后的产品列表到内存中,对内存有一定要求。
  • 内存搜索与分页效率:对于极大的聚合后列表,内存操作的效率也需要关注。

应对策略:

  1. 优化 findAllByAdminId:确保 adminId 有索引。如果业务允许,增加如日期范围等硬性条件来减少加载的流水量。
  2. 数据库层面预聚合/物化视图:对于性能要求极高或数据量超大的场景,这是王道。
  3. 逐步加载与缓存:如果聚合结果相对稳定,可以考虑缓存。
  4. 评估数据量级:明确系统需要处理的数据规模,是选择优化策略的基础。

这个方案为构建复杂的、用户体验良好的动态报表提供了一个坚实的后端基础。希望它能启发你在类似项目中的设计!编码愉快,持续探索!🌟


🧠 思维导图 (Markdown 格式)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值