商品目录服务 - 跨境电商的商品展示引擎

# 商品目录服务 - 跨境电商的商品展示引擎

# 📖 故事开始

小明是一位在深圳工作的程序员,最近想给远在美国的女朋友买一份生日礼物。他打开了一个跨境电商平台,准备寻找一款心仪的手表。当他在搜索框输入"瑞士手表"时,页面瞬间展示出了数百款精美的手表,每款都有详细的图片、价格、规格参数,甚至还有用户评价。

这看似简单的商品展示背后,隐藏着一个复杂而精密的商品目录服务系统。今天,我们就来揭开这个系统的神秘面纱。

# 🎯 系统概述

商品目录服务是跨境电商平台的核心基础服务,负责管理和展示平台上的所有商品信息。它需要处理海量的商品数据,支持多语言、多货币、多地区的展示需求,同时保证高性能和高可用性。

# 核心功能

  • 商品信息管理(CRUD操作)
  • 多语言商品展示
  • 多货币价格转换
  • 商品分类管理
  • 库存状态同步
  • 商品搜索支持

# 🏗️ 系统架构设计

# 整体架构图

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   前端应用      │    │   API网关       │    │   商品目录服务  │
│                 │────│                 │────│                 │
│ - Web端         │    │ - 路由转发      │    │ - 商品管理      │
│ - 移动端        │    │ - 限流熔断      │    │ - 分类管理      │
│ - 小程序        │    │ - 认证授权      │    │ - 价格计算      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                                        │
                                               ┌─────────────────┐
                                               │   数据存储层    │
                                               │                 │
                                               │ - MySQL主库     │
                                               │ - Redis缓存     │
                                               │ - ES搜索引擎    │
                                               │ - CDN图片存储   │
                                               └─────────────────┘

# 💻 核心代码实现

# 1. 商品实体模型设计

/**
 * 商品实体类
 * 设计思路:采用多语言支持的设计模式,将基础信息和多语言信息分离
 */
@Entity
@Table(name = "products")
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 商品SKU - 全球唯一标识
     * 格式:品牌代码(2位) + 分类代码(4位) + 序列号(8位)
     * 例如:AP001200012345678 (Apple手表)
     */
    @Column(unique = true, nullable = false, length = 20)
    private String sku;
    
    /**
     * 品牌ID - 关联品牌表
     */
    @Column(name = "brand_id")
    private Long brandId;
    
    /**
     * 分类ID - 关联分类表
     */
    @Column(name = "category_id")
    private Long categoryId;
    
    /**
     * 基础价格(美元)- 所有价格计算的基准
     */
    @Column(name = "base_price", precision = 10, scale = 2)
    private BigDecimal basePrice;
    
    /**
     * 商品状态:1-上架,2-下架,3-预售,4-停产
     */
    @Column(name = "status")
    private Integer status;
    
    /**
     * 重量(克)- 用于物流费用计算
     */
    @Column(name = "weight")
    private Integer weight;
    
    /**
     * 尺寸信息(长x宽x高,单位:厘米)
     */
    @Column(name = "dimensions")
    private String dimensions;
    
    /**
     * 创建时间
     */
    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    /**
     * 更新时间
     */
    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // 构造函数、getter、setter省略...
}

/**
 * 商品多语言信息实体
 * 设计思路:支持商品信息的多语言展示
 */
@Entity
@Table(name = "product_i18n")
public class ProductI18n {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    /**
     * 关联的商品ID
     */
    @Column(name = "product_id")
    private Long productId;
    
    /**
     * 语言代码:en-US, zh-CN, ja-JP等
     */
    @Column(name = "language_code", length = 10)
    private String languageCode;
    
    /**
     * 商品名称
     */
    @Column(name = "name", length = 500)
    private String name;
    
    /**
     * 商品描述
     */
    @Column(name = "description", columnDefinition = "TEXT")
    private String description;
    
    /**
     * 商品特性(JSON格式存储)
     * 例如:{"颜色": "黑色", "尺寸": "42mm", "材质": "不锈钢"}
     */
    @Column(name = "features", columnDefinition = "JSON")
    private String features;
    
    // 构造函数、getter、setter省略...
}

# 2. 商品服务核心业务逻辑

/**
 * 商品目录服务实现类
 * 核心职责:商品信息的增删改查、多语言支持、价格计算
 */
@Service
@Transactional
public class ProductCatalogService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private ProductI18nRepository productI18nRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private CurrencyExchangeService currencyExchangeService;
    
    /**
     * 获取商品详情(支持多语言和多货币)
     * 
     * @param productId 商品ID
     * @param languageCode 语言代码
     * @param currencyCode 货币代码
     * @param countryCode 国家代码(用于税费计算)
     * @return 商品详情DTO
     */
    public ProductDetailDTO getProductDetail(Long productId, String languageCode, 
                                           String currencyCode, String countryCode) {
        
        // 1. 构建缓存键
        String cacheKey = String.format("product:detail:%d:%s:%s:%s", 
                                       productId, languageCode, currencyCode, countryCode);
        
        // 2. 尝试从Redis缓存获取
        ProductDetailDTO cachedProduct = (ProductDetailDTO) redisTemplate.opsForValue().get(cacheKey);
        if (cachedProduct != null) {
            log.info("从缓存获取商品详情,productId: {}", productId);
            return cachedProduct;
        }
        
        // 3. 从数据库查询基础商品信息
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new ProductNotFoundException("商品不存在,ID: " + productId));
        
        // 4. 查询多语言信息
        ProductI18n productI18n = productI18nRepository
                .findByProductIdAndLanguageCode(productId, languageCode)
                .orElse(getDefaultLanguageProduct(productId)); // 如果没有对应语言,使用默认语言
        
        // 5. 构建商品详情DTO
        ProductDetailDTO productDetail = buildProductDetailDTO(product, productI18n, 
                                                              currencyCode, countryCode);
        
        // 6. 存入缓存(过期时间30分钟)
        redisTemplate.opsForValue().set(cacheKey, productDetail, Duration.ofMinutes(30));
        
        log.info("商品详情查询完成,productId: {}, language: {}, currency: {}", 
                productId, languageCode, currencyCode);
        
        return productDetail;
    }
    
    /**
     * 构建商品详情DTO
     * 核心逻辑:价格计算、税费计算、库存状态获取
     */
    private ProductDetailDTO buildProductDetailDTO(Product product, ProductI18n productI18n,
                                                  String currencyCode, String countryCode) {
        
        ProductDetailDTO dto = new ProductDetailDTO();
        
        // 基础信息映射
        dto.setId(product.getId());
        dto.setSku(product.getSku());
        dto.setName(productI18n.getName());
        dto.setDescription(productI18n.getDescription());
        dto.setFeatures(parseFeatures(productI18n.getFeatures()));
        dto.setWeight(product.getWeight());
        dto.setDimensions(product.getDimensions());
        dto.setStatus(product.getStatus());
        
        // 价格计算(这是核心业务逻辑)
        PriceCalculationResult priceResult = calculatePrice(product.getBasePrice(), 
                                                          currencyCode, countryCode);
        dto.setPriceInfo(priceResult);
        
        // 库存信息获取
        InventoryInfo inventoryInfo = getInventoryInfo(product.getSku());
        dto.setInventoryInfo(inventoryInfo);
        
        // 图片信息获取
        List<String> imageUrls = getProductImages(product.getId());
        dto.setImageUrls(imageUrls);
        
        return dto;
    }
    
    /**
     * 价格计算核心算法
     * 计算逻辑:基础价格 -> 汇率转换 -> 税费计算 -> 最终价格
     */
    private PriceCalculationResult calculatePrice(BigDecimal basePrice, 
                                                String currencyCode, String countryCode) {
        
        PriceCalculationResult result = new PriceCalculationResult();
        
        try {
            // 1. 汇率转换
            BigDecimal exchangeRate = currencyExchangeService.getExchangeRate("USD", currencyCode);
            BigDecimal convertedPrice = basePrice.multiply(exchangeRate)
                    .setScale(2, RoundingMode.HALF_UP);
            
            // 2. 税费计算(根据目标国家)
            TaxCalculationResult taxResult = calculateTax(convertedPrice, countryCode);
            
            // 3. 最终价格计算
            BigDecimal finalPrice = convertedPrice.add(taxResult.getTaxAmount());
            
            // 4. 构建价格结果
            result.setOriginalPrice(basePrice);
            result.setConvertedPrice(convertedPrice);
            result.setTaxAmount(taxResult.getTaxAmount());
            result.setTaxRate(taxResult.getTaxRate());
            result.setFinalPrice(finalPrice);
            result.setCurrencyCode(currencyCode);
            result.setExchangeRate(exchangeRate);
            
            log.debug("价格计算完成 - 基础价格: {} USD, 转换价格: {} {}, 税费: {} {}, 最终价格: {} {}",
                    basePrice, convertedPrice, currencyCode, 
                    taxResult.getTaxAmount(), currencyCode, finalPrice, currencyCode);
            
        } catch (Exception e) {
            log.error("价格计算失败", e);
            throw new PriceCalculationException("价格计算失败: " + e.getMessage());
        }
        
        return result;
    }
    
    /**
     * 税费计算
     * 不同国家有不同的税率政策
     */
    private TaxCalculationResult calculateTax(BigDecimal price, String countryCode) {
        
        TaxCalculationResult result = new TaxCalculationResult();
        
        // 根据国家代码获取税率配置
        BigDecimal taxRate = getTaxRateByCountry(countryCode);
        BigDecimal taxAmount = price.multiply(taxRate)
                .setScale(2, RoundingMode.HALF_UP);
        
        result.setTaxRate(taxRate);
        result.setTaxAmount(taxAmount);
        result.setTaxType(getTaxTypeByCountry(countryCode));
        
        return result;
    }
    
    /**
     * 根据国家获取税率
     * 实际项目中这些数据应该从配置表或外部税务服务获取
     */
    private BigDecimal getTaxRateByCountry(String countryCode) {
        Map<String, BigDecimal> taxRates = Map.of(
            "US", new BigDecimal("0.08"),    // 美国 8%
            "CN", new BigDecimal("0.13"),    // 中国 13%
            "JP", new BigDecimal("0.10"),    // 日本 10%
            "DE", new BigDecimal("0.19"),    // 德国 19%
            "GB", new BigDecimal("0.20")     // 英国 20%
        );
        
        return taxRates.getOrDefault(countryCode, new BigDecimal("0.10")); // 默认10%
    }
    
    /**
     * 批量获取商品列表(支持分页和筛选)
     */
    public PageResult<ProductListItemDTO> getProductList(ProductQueryRequest request) {
        
        // 1. 构建查询条件
        Specification<Product> spec = buildProductSpecification(request);
        
        // 2. 构建分页参数
        Pageable pageable = PageRequest.of(
            request.getPageNum() - 1, 
            request.getPageSize(),
            Sort.by(Sort.Direction.DESC, "createdAt")
        );
        
        // 3. 执行查询
        Page<Product> productPage = productRepository.findAll(spec, pageable);
        
        // 4. 转换为DTO
        List<ProductListItemDTO> productList = productPage.getContent().stream()
                .map(product -> convertToListItemDTO(product, request.getLanguageCode(), 
                                                   request.getCurrencyCode(), request.getCountryCode()))
                .collect(Collectors.toList());
        
        // 5. 构建分页结果
        return PageResult.<ProductListItemDTO>builder()
                .data(productList)
                .total(productPage.getTotalElements())
                .pageNum(request.getPageNum())
                .pageSize(request.getPageSize())
                .build();
    }
    
    /**
     * 构建商品查询条件
     * 支持按分类、品牌、价格区间、关键词等筛选
     */
    private Specification<Product> buildProductSpecification(ProductQueryRequest request) {
        return (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            // 商品状态过滤(只显示上架商品)
            predicates.add(criteriaBuilder.equal(root.get("status"), ProductStatus.ACTIVE.getCode()));
            
            // 分类筛选
            if (request.getCategoryId() != null) {
                predicates.add(criteriaBuilder.equal(root.get("categoryId"), request.getCategoryId()));
            }
            
            // 品牌筛选
            if (request.getBrandId() != null) {
                predicates.add(criteriaBuilder.equal(root.get("brandId"), request.getBrandId()));
            }
            
            // 价格区间筛选
            if (request.getMinPrice() != null) {
                predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("basePrice"), request.getMinPrice()));
            }
            if (request.getMaxPrice() != null) {
                predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("basePrice"), request.getMaxPrice()));
            }
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }
}

# 3. 缓存策略设计

/**
 * 商品缓存管理服务
 * 设计思路:多级缓存 + 缓存预热 + 缓存更新策略
 */
@Service
public class ProductCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    // 缓存键前缀常量
    private static final String PRODUCT_DETAIL_PREFIX = "product:detail:";
    private static final String PRODUCT_LIST_PREFIX = "product:list:";
    private static final String HOT_PRODUCTS_KEY = "product:hot:list";
    
    /**
     * 缓存预热 - 系统启动时预加载热门商品
     */
    @PostConstruct
    public void warmUpCache() {
        log.info("开始商品缓存预热...");
        
        // 获取热门商品列表(根据销量排序的前100个商品)
        List<Product> hotProducts = productRepository.findTop100ByOrderBySalesCountDesc();
        
        // 预热多语言版本的商品详情
        List<String> languages = Arrays.asList("en-US", "zh-CN", "ja-JP", "de-DE");
        List<String> currencies = Arrays.asList("USD", "CNY", "JPY", "EUR");
        List<String> countries = Arrays.asList("US", "CN", "JP", "DE");
        
        for (Product product : hotProducts) {
            for (int i = 0; i < languages.size(); i++) {
                String cacheKey = buildCacheKey(product.getId(), languages.get(i), 
                                              currencies.get(i), countries.get(i));
                
                // 异步预热缓存
                CompletableFuture.runAsync(() -> {
                    try {
                        // 这里调用实际的商品详情获取方法
                        // getProductDetail方法会自动将结果存入缓存
                        // productCatalogService.getProductDetail(product.getId(), 
                        //     languages.get(i), currencies.get(i), countries.get(i));
                    } catch (Exception e) {
                        log.warn("缓存预热失败,商品ID: {}, 语言: {}", product.getId(), languages.get(i));
                    }
                });
            }
        }
        
        log.info("商品缓存预热完成,预热商品数量: {}", hotProducts.size());
    }
    
    /**
     * 缓存失效策略 - 当商品信息更新时
     */
    public void invalidateProductCache(Long productId) {
        
        // 获取所有可能的缓存键模式
        String pattern = PRODUCT_DETAIL_PREFIX + productId + ":*";
        
        // 查找所有匹配的缓存键
        Set<String> keys = redisTemplate.keys(pattern);
        
        if (!keys.isEmpty()) {
            // 批量删除缓存
            redisTemplate.delete(keys);
            log.info("商品缓存失效完成,商品ID: {}, 删除缓存键数量: {}", productId, keys.size());
        }
        
        // 同时清除商品列表缓存(因为商品信息变更可能影响列表展示)
        invalidateProductListCache();
    }
    
    /**
     * 清除商品列表缓存
     */
    public void invalidateProductListCache() {
        String pattern = PRODUCT_LIST_PREFIX + "*";
        Set<String> keys = redisTemplate.keys(pattern);
        
        if (!keys.isEmpty()) {
            redisTemplate.delete(keys);
            log.info("商品列表缓存清除完成,删除缓存键数量: {}", keys.size());
        }
    }
    
    /**
     * 构建缓存键
     */
    private String buildCacheKey(Long productId, String languageCode, 
                                String currencyCode, String countryCode) {
        return String.format("%s%d:%s:%s:%s", PRODUCT_DETAIL_PREFIX, 
                           productId, languageCode, currencyCode, countryCode);
    }
}

# 🔧 技术要点解析

# 1. 多语言支持设计

设计原理:采用主表+多语言表的设计模式,将不变的基础信息(如价格、重量)存储在主表,将可变的多语言信息(如名称、描述)存储在多语言表。

优势

  • 数据结构清晰,便于维护
  • 支持动态添加新语言
  • 查询性能优秀

# 2. 价格计算策略

计算流程

  1. 基础价格(USD)→ 汇率转换 → 目标货币价格
  2. 目标货币价格 → 税费计算 → 最终价格

关键考虑

  • 汇率实时性:集成第三方汇率服务
  • 税费准确性:根据目标国家税法计算
  • 精度控制:使用BigDecimal避免浮点数精度问题

# 3. 缓存策略优化

多级缓存架构

  • L1缓存:应用内存缓存(Caffeine)
  • L2缓存:Redis分布式缓存
  • L3缓存:CDN边缘缓存

缓存更新策略

  • 写入时:Cache Aside模式
  • 失效时:基于事件的主动失效
  • 预热时:系统启动时预加载热门数据

# 📊 性能优化实践

# 1. 数据库优化

-- 商品表索引设计
CREATE INDEX idx_product_category_status ON products(category_id, status);
CREATE INDEX idx_product_brand_status ON products(brand_id, status);
CREATE INDEX idx_product_price_range ON products(base_price, status);
CREATE INDEX idx_product_created_at ON products(created_at DESC);

-- 多语言表索引设计
CREATE UNIQUE INDEX idx_product_i18n_product_lang ON product_i18n(product_id, language_code);
CREATE INDEX idx_product_i18n_language ON product_i18n(language_code);

# 2. 查询优化

/**
 * 批量查询优化 - 减少N+1查询问题
 */
@Query("SELECT p FROM Product p " +
       "LEFT JOIN FETCH p.brand " +
       "LEFT JOIN FETCH p.category " +
       "WHERE p.status = :status")
List<Product> findActiveProductsWithDetails(@Param("status") Integer status);

/**
 * 分页查询优化 - 使用游标分页
 */
public Page<Product> findProductsWithCursor(String cursor, int size) {
    // 使用创建时间作为游标,避免深分页性能问题
    LocalDateTime cursorTime = cursor != null ? 
        LocalDateTime.parse(cursor) : LocalDateTime.now();
    
    Pageable pageable = PageRequest.of(0, size);
    return productRepository.findByCreatedAtLessThanOrderByCreatedAtDesc(cursorTime, pageable);
}

# 🚀 部署与监控

# 1. 容器化部署

# Dockerfile
FROM openjdk:11-jre-slim

# 设置工作目录
WORKDIR /app

# 复制应用JAR文件
COPY target/product-catalog-service.jar app.jar

# 设置JVM参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

# 启动应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

# 2. 监控指标

/**
 * 自定义监控指标
 */
@Component
public class ProductMetrics {
    
    private final Counter productViewCounter;
    private final Timer productQueryTimer;
    private final Gauge cacheHitRateGauge;
    
    public ProductMetrics(MeterRegistry meterRegistry) {
        this.productViewCounter = Counter.builder("product.view.count")
                .description("商品查看次数")
                .register(meterRegistry);
                
        this.productQueryTimer = Timer.builder("product.query.duration")
                .description("商品查询耗时")
                .register(meterRegistry);
                
        this.cacheHitRateGauge = Gauge.builder("product.cache.hit.rate")
                .description("商品缓存命中率")
                .register(meterRegistry, this, ProductMetrics::calculateCacheHitRate);
    }
    
    public void recordProductView(String productId) {
        productViewCounter.increment(Tags.of("productId", productId));
    }
    
    public Timer.Sample startQueryTimer() {
        return Timer.start();
    }
    
    public void recordQueryTime(Timer.Sample sample) {
        sample.stop(productQueryTimer);
    }
    
    private double calculateCacheHitRate() {
        // 计算缓存命中率的逻辑
        return 0.95; // 示例值
    }
}

# 🎉 总结

通过本文的学习,我们深入了解了跨境电商商品目录服务的设计与实现。从小明搜索瑞士手表的简单操作,到背后复杂的多语言、多货币、多地区支持,我们看到了一个看似简单的功能背后蕴含的技术深度。

# 关键收获

  1. 架构设计:采用微服务架构,职责分离,便于扩展和维护
  2. 数据模型:主表+多语言表的设计模式,支持国际化需求
  3. 性能优化:多级缓存、索引优化、批量查询等技术手段
  4. 业务逻辑:价格计算、税费处理等核心业务算法
  5. 监控运维:完善的监控指标和健康检查机制

# 最佳实践

  • 使用BigDecimal处理金额计算,避免精度问题
  • 采用多级缓存策略,提升系统性能
  • 设计合理的数据库索引,优化查询性能
  • 实现完善的监控和告警机制
  • 考虑国际化需求,支持多语言和多货币

下一篇文章,我们将继续探讨跨境电商交易链路的下一个环节:搜索推荐引擎,看看如何让用户更快地找到心仪的商品。