场景

一个商城系统从第三方接口获取了一批商品列表,但运营人员在后台配置了某些商品"不展示"。

需要根据数据库配置表动态过滤掉这些商品,只返回允许展示的商品。

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi 

一、建表 SQL

CREATE TABLE `t_product_display_config` (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
  `product_code` varchar(50) NOT NULL COMMENT '商品编码',
  `product_name` varchar(200) DEFAULT NULL COMMENT '商品名称',
  `is_visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否展示 1是 0否',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_code` (`product_code`)
) ENGINE=InnoDB DEFAULT charset=utf8mb4 COMMENT='商品展示配置表';

-- 示例数据

INSERT INTO t_product_display_config (product_code, product_name, is_visible) VALUES
('P001', '冰箱A', 1),   -- 展示
('P003', '洗衣机C', 0), -- 不展示
('P005', '空调E', 0);   -- 不展示

二、实体类

package com.example.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;

@TableName("t_product_display_config")
@Data
public class ProductDisplayConfigEntity {

    @TableId(type = IdType.AUTO)
    private Long id;
    private String productCode;
    private String productName;
    private Integer isVisible;
    private Date createTime;
    private Date updateTime;
}

三、Mapper

package com.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.ProductDisplayConfigEntity;

public interface ProductDisplayConfigMapper extends BaseMapper<ProductDisplayConfigEntity> {
}

不需要写 XML,MyBatis-Plus 的 BaseMapper 已经提供了 selectList 方法。

四、第三方接口返回的数据结构

package com.example.dto;

import lombok.Data;
import java.math.BigDecimal;

/**
 * 第三方接口返回的商品数据
 */
@Data
public class ThirdPartyProduct {
    private String productCode;    // 商品编码
    private String productName;    // 商品名称
    private BigDecimal price;      // 价格
    private String category;       // 分类
}

五、Service 实现(核心过滤逻辑)

package com.example.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.dto.ThirdPartyProduct;
import com.example.entity.ProductDisplayConfigEntity;
import com.example.mapper.ProductDisplayConfigMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import cn.hutool.core.util.StrUtil;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Service
public class ProductServiceImpl {

    private final ProductDisplayConfigMapper displayConfigMapper;
    private final ThirdPartyProductFeign thirdPartyFeign;

    public ProductServiceImpl(ProductDisplayConfigMapper displayConfigMapper,
                              ThirdPartyProductFeign thirdPartyFeign) {
        this.displayConfigMapper = displayConfigMapper;
        this.thirdPartyFeign = thirdPartyFeign;
    }

    /**
     * 获取商品列表(过滤掉配置为不展示的商品)
     */
    public List<ThirdPartyProduct> getVisibleProducts(String category) {
        // 1. 从第三方接口获取商品列表
        List<ThirdPartyProduct> allProducts = thirdPartyFeign.getProducts(category);

        if (CollectionUtils.isEmpty(allProducts)) {
            return allProducts;
        }

        // 2. 过滤掉配置为不展示的商品
        return filterByDisplayConfig(allProducts);
    }

    /**
     * 根据展示配置表过滤商品
     *
     * 过滤规则:
     * - 商品编码不在配置表中 → 展示
     * - 商品编码在配置表中且 is_visible=1 → 展示
     * - 商品编码在配置表中且 is_visible=0 → 不展示
     */
    private List<ThirdPartyProduct> filterByDisplayConfig(List<ThirdPartyProduct> productList) {

        // ===== 第一步:从原始数据中提取所有商品编码(去空、去重) =====
        List<String> productCodes = productList.stream()
                .map(ThirdPartyProduct::getProductCode)   // 提取商品编码字段
                .filter(StrUtil::isNotBlank)              // 过滤掉空值
                .distinct()                               // 去重
                .collect(Collectors.toList());            // 收集为 List

        // 提前返回:没有有效编码,无需查库
        if (CollectionUtils.isEmpty(productCodes)) {
            return productList;
        }

        // ===== 第二步:查询配置表中"存在且不展示"的商品编码 =====
        LambdaQueryWrapper<ProductDisplayConfigEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(ProductDisplayConfigEntity::getProductCode, productCodes);  // WHERE product_code IN (...)
        queryWrapper.eq(ProductDisplayConfigEntity::getIsVisible, 0);               // AND is_visible = 0
        List<ProductDisplayConfigEntity> hiddenList = displayConfigMapper.selectList(queryWrapper);

        // 提前返回:没有需要隐藏的商品
        if (CollectionUtils.isEmpty(hiddenList)) {
            return productList;
        }

        // ===== 第三步:构建"黑名单"集合(用 HashSet 实现 O(1) 查找) =====
        Set<String> hiddenProductCodes = hiddenList.stream()
                .map(ProductDisplayConfigEntity::getProductCode)
                .collect(Collectors.toSet());

        // ===== 第四步:过滤原始列表,排除黑名单中的商品 =====
        return productList.stream()
                .filter(product -> !hiddenProductCodes.contains(product.getProductCode()))
                .collect(Collectors.toList());
    }
}

六、执行过程演示

假设第三方接口返回 5 个商品:

商品编码 商品名称
P001 冰箱A
P002 冰箱B
P003 洗衣机C
P004 洗衣机D
P005 空调E

数据库配置表中的数据:

product_code is_visible
P001 1(展示)
P003 0(不展示)
P005 0(不展示)

执行过程:

第一步:提取编码 → [P001, P002, P003, P004, P005]

第二步:查库(只查 is_visible= 0 的)

SQL: SELECT * FROM t_product_display_config
       WHERE product_code IN ('P001','P002','P003','P004','P005')
       AND is_visible = 0

结果:[P003, P005]

第三步:构建黑名单 Set → {P003, P005}

第四步:过滤

P001 → 不在黑名单 → 保留 ✅

P002 → 不在黑名单 → 保留 ✅

P003 → 在黑名单   → 排除 ❌

P004 → 不在黑名单 → 保留 ✅

P005 → 在黑名单   → 排除 ❌

最终返回:[P001, P002, P004]

七、关键知识点逐个拆解

1. Stream 的 map + filter + distinct + collect

List<String> codes = list.stream()
    .map(Item::getCode)        // 从对象中提取某个字段,List<Item> → Stream<String>
    .filter(StrUtil::isNotBlank) // 只保留非空的
    .distinct()                 // 去重
    .collect(Collectors.toList()); // 收集为新的 List

等价的传统写法:

Set<String> seen = new HashSet<>();
List<String> codes = new ArrayList<>();
for (Item item : list) {
    String code = item.getCode();
    if (code != null && !code.isEmpty() && !seen.contains(code)) {
        seen.add(code);
        codes.add(code);
    }
}

2. LambdaQueryWrapper 构建查询条件

LambdaQueryWrapper<Entity> qw = new LambdaQueryWrapper<>();
qw.in(Entity::getField, valueList);   // WHERE field IN (v1, v2, v3)
qw.eq(Entity::getStatus, 0);          // AND status = 0
qw.like(Entity::getName, "关键词");    // AND name LIKE '%关键词%'
qw.between(Entity::getAge, 18, 60);   // AND age BETWEEN 18 AND 60

3. HashSet 的 O(1) contains 判断

// 构建 Set
Set<String> blacklist = hiddenList.stream()
    .map(Entity::getCode)
    .collect(Collectors.toSet());  // HashSet

// 使用 Set 判断
blacklist.contains("P003");  // O(1),直接哈希定位

原理: HashSet 内部是 HashMap,contains() 通过计算 hashCode 直接定位桶位置,不需要遍历。

性能对比(过滤 1000 条数据,黑名单 100 个):

List.contains():1000 × 100 = 10 万次比较

Set.contains():1000 × 1 = 1000 次哈希查找

4. Stream 的 filter 过滤

// 保留"不在黑名单中"的元素
list.stream()
    .filter(item -> !blacklist.contains(item.getCode()))  // 返回 true 的保留
    .collect(Collectors.toList());                         // 收集为新 List

注意:

filter 是保留满足条件的元素,不是删除。!blacklist.contains(...) 表示"不在黑名单中的保留"。

5. Early Return(提前返回)

// 场景1:输入为空,无需处理
if (CollectionUtils.isEmpty(productCodes)) {
    return productList;
}

// 场景2:查库结果为空,无需过滤
if (CollectionUtils.isEmpty(hiddenList)) {
    return productList;
}

好处:

避免不必要的数据库查询(省一次网络 IO)

减少代码嵌套(不用写 if (xxx != null && !xxx.isEmpty()) { ... })

八、总结

整个流程可以概括为四步:

提取 Key → 查库获取黑名单 → 构建 HashSet → Stream 过滤

提取 Key stream().map().filter().distinct() 从对象列表中提取唯一标识

查库 LambdaQueryWrapper + IN 查询 获取需要排除的记录

构建集合 Collectors.toSet() → HashSet O(1) 的快速查找

过滤 stream().filter().collect() 返回新列表,排除黑名单项

这个模式适用于所有"根据配置动态过滤数据"的场景:商品上下架、权限控制、内容审核、灰度发布等。

逻辑更清晰:先排除特殊情况,再处理正常逻辑

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐