商品目录服务 - 跨境电商的商品展示引擎
# 商品目录服务 - 跨境电商的商品展示引擎
# 📖 故事开始
小明是一位在深圳工作的程序员,最近想给远在美国的女朋友买一份生日礼物。他打开了一个跨境电商平台,准备寻找一款心仪的手表。当他在搜索框输入"瑞士手表"时,页面瞬间展示出了数百款精美的手表,每款都有详细的图片、价格、规格参数,甚至还有用户评价。
这看似简单的商品展示背后,隐藏着一个复杂而精密的商品目录服务系统。今天,我们就来揭开这个系统的神秘面纱。
# 🎯 系统概述
商品目录服务是跨境电商平台的核心基础服务,负责管理和展示平台上的所有商品信息。它需要处理海量的商品数据,支持多语言、多货币、多地区的展示需求,同时保证高性能和高可用性。
# 核心功能
- 商品信息管理(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. 价格计算策略
计算流程:
- 基础价格(USD)→ 汇率转换 → 目标货币价格
- 目标货币价格 → 税费计算 → 最终价格
关键考虑:
- 汇率实时性:集成第三方汇率服务
- 税费准确性:根据目标国家税法计算
- 精度控制:使用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 \
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; // 示例值
}
}
# 🎉 总结
通过本文的学习,我们深入了解了跨境电商商品目录服务的设计与实现。从小明搜索瑞士手表的简单操作,到背后复杂的多语言、多货币、多地区支持,我们看到了一个看似简单的功能背后蕴含的技术深度。
# 关键收获
- 架构设计:采用微服务架构,职责分离,便于扩展和维护
- 数据模型:主表+多语言表的设计模式,支持国际化需求
- 性能优化:多级缓存、索引优化、批量查询等技术手段
- 业务逻辑:价格计算、税费处理等核心业务算法
- 监控运维:完善的监控指标和健康检查机制
# 最佳实践
- 使用BigDecimal处理金额计算,避免精度问题
- 采用多级缓存策略,提升系统性能
- 设计合理的数据库索引,优化查询性能
- 实现完善的监控和告警机制
- 考虑国际化需求,支持多语言和多货币
下一篇文章,我们将继续探讨跨境电商交易链路的下一个环节:搜索推荐引擎,看看如何让用户更快地找到心仪的商品。