电商返利系统多平台API适配器模式实践
1. 电商返利系统的多联盟API对接挑战在电商返利系统的开发过程中最头疼的问题莫过于要同时对接多个电商平台的联盟API。淘宝联盟、京东联盟、拼多多联盟这些平台每个都有自己的接口规范、参数格式和签名机制。就拿最简单的商品查询接口来说淘宝联盟用的是Taobao Open API需要传app_key、timestamp这些参数返回XML格式数据京东联盟则是HTTPJSON接口要求每个请求都要带签名拼多多的API又完全是另一套玩法更麻烦的是这些平台的SDK和接口还经常升级变动。记得去年双11前淘宝突然更新了商品详情接口导致我们系统直接瘫痪了半天。这种强耦合的架构让维护成本居高不下每次对接新平台或者接口变动都得把业务逻辑改一遍。2. 适配器模式的核心设计思路2.1 统一接口抽象我们采用适配器模式(Adapter Pattern)来解决这个问题。首先定义一个统一的联盟接口UnionApi抽象出两个核心能力public interface UnionApi { // 商品查询 UnionItem queryItem(String itemId); // 生成推广链接 UnionLink generatePromotionLink(String itemId, String adzoneId); }这个接口就像是各种联盟API的通用插座不管底层对接的是哪个平台对外都提供统一的调用方式。UnionItem和UnionLink则是我们自定义的统一数据模型public class UnionItem { private String itemId; // 商品ID private String title; // 商品标题 private BigDecimal couponAmount; // 优惠券金额 private BigDecimal commissionRate; // 佣金比例 // 其他字段... }2.2 具体平台适配器实现对于每个电商平台我们实现一个对应的适配器类。以淘宝联盟为例Component(taobaoUnionApi) public class TaobaoUnionApiAdapter implements UnionApi { Value(${union.taobao.app-key}) private String appKey; Override public UnionItem queryItem(String itemId) { // 调用淘宝官方SDK DefaultTaobaoClient client new DefaultTaobaoClient(API_URL, appKey, appSecret); TbkItemInfoGetRequest req new TbkItemInfoGetRequest(); req.setNumIids(itemId); // 将淘宝返回的数据转换为我们统一的UnionItem TbkItemInfoGetResponse resp client.execute(req); UnionItem item new UnionItem(); item.setItemId(resp.getResults().get(0).getNumIid()); item.setTitle(resp.getResults().get(0).getTitle()); // 其他字段转换... return item; } }京东联盟的适配器稍有不同因为京东提供的是HTTP接口Component(jdUnionApi) public class JdUnionApiAdapter implements UnionApi { Override public UnionItem queryItem(String skuId) { // 构造京东API请求 String url https://router.jd.com/api; MapString, String params new HashMap(); params.put(method, jingdong.service.promotion.get); params.put(skuIds, skuId); // 添加签名等必要参数 // 发送请求并转换响应 ResponseEntityMap resp restTemplate.getForEntity(url, Map.class, params); Map data (Map) resp.getBody().get(jingdong_service_promotion_get_responce); // 转换为统一模型 UnionItem item new UnionItem(); item.setItemId(skuId); item.setTitle((String) data.get(wareName)); // 其他字段... return item; } }3. 上下文路由与动态调用3.1 平台路由工厂为了让业务代码能透明地调用不同平台的适配器我们实现了一个UnionApiContextComponent public class UnionApiContext { Resource private MapString, UnionApi unionApiBeans; // Spring会自动注入所有UnionApi实现 public UnionApi getApi(String platform) { switch (platform.toLowerCase()) { case taobao: return unionApiBeans.get(taobaoUnionApi); case jd: return unionApiBeans.get(jdUnionApi); case pdd: return unionApiBeans.get(pddUnionApi); default: throw new IllegalArgumentException(Unsupported platform); } } }3.2 业务层调用示例业务代码只需要知道要调用哪个平台完全不用关心具体API细节Service public class ItemService { private final UnionApiContext unionApiContext; public UnionItem getItemInfo(String platform, String itemId) { UnionApi api unionApiContext.getApi(platform); return api.queryItem(itemId); } public UnionLink generateLink(String platform, String itemId, String adzoneId) { UnionApi api unionApiContext.getApi(platform); return api.generatePromotionLink(itemId, adzoneId); } }4. 实战中的经验与优化4.1 性能优化技巧在实际运行中我们发现频繁创建API客户端会影响性能。比如淘宝的DefaultTaobaoClient每次创建都要初始化一些资源。优化方法是使用对象池Component public class TaobaoClientPool { private final ObjectPoolDefaultTaobaoClient pool; public TaobaoClientPool() { this.pool new GenericObjectPool(new BasePooledObjectFactory() { Override public DefaultTaobaoClient create() { return new DefaultTaobaoClient(API_URL, appKey, appSecret); } }); } public T T execute(FunctionDefaultTaobaoClient, T callback) { DefaultTaobaoClient client pool.borrowObject(); try { return callback.apply(client); } catch (Exception e) { throw new RuntimeException(API调用失败, e); } finally { pool.returnObject(client); } } }适配器中可以这样使用public UnionItem queryItem(String itemId) { return clientPool.execute(client - { TbkItemInfoGetRequest req new TbkItemInfoGetRequest(); req.setNumIids(itemId); TbkItemInfoGetResponse resp client.execute(req); // 转换逻辑... }); }4.2 异常处理策略不同平台的异常处理方式也不统一。我们定义了统一的异常体系public class UnionApiException extends RuntimeException { private String platform; private String errorCode; // 构造方法等... } // 在适配器中转换异常 try { // 调用平台API } catch (TaobaoApiException e) { throw new UnionApiException(taobao, e.getErrorCode(), e.getMessage(), e); } catch (JdApiException e) { throw new UnionApiException(jd, e.getCode(), e.getMsg(), e); }业务层可以统一捕获UnionApiException根据platform和errorCode做相应处理。4.3 监控与日志为了监控各平台API的调用情况我们使用Spring AOP做了切面Aspect Component public class ApiMonitorAspect { Around(execution(* juwatech.cn.union.spi.UnionApi.*(..))) public Object monitorApiCall(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); String methodName pjp.getSignature().getName(); String platform pjp.getTarget().getClass().getSimpleName() .replace(UnionApiAdapter, ).toLowerCase(); try { Object result pjp.proceed(); long duration System.currentTimeMillis() - start; log.info({} API调用成功 - {}: {}ms, platform, methodName, duration); metrics.recordSuccess(platform, methodName, duration); return result; } catch (Exception e) { metrics.recordError(platform, methodName); throw e; } } }5. 扩展新平台的实践当需要接入新平台时比如最近要加的抖音电商只需要实现UnionApi接口添加新的适配器类在UnionApiContext中添加路由逻辑完全不需要修改现有业务代码。以抖音为例Component(douyinUnionApi) public class DouyinUnionApiAdapter implements UnionApi { Override public UnionItem queryItem(String itemId) { // 调用抖音联盟API DouyinProduct product douyinClient.getProduct(itemId); // 转换为统一模型 UnionItem item new UnionItem(); item.setItemId(product.getProductId()); item.setTitle(product.getTitle()); // 其他字段... return item; } } // 在UnionApiContext中添加 case douyin: return unionApiBeans.get(douyinUnionApi);6. 配置管理的优化随着对接平台增多配置项也越来越多。我们改用配置中心管理# union-api-config taobao: app-key: ${TAOBAO_APP_KEY} app-secret: ${TAOBAO_APP_SECRET} api-url: https://eco.taobao.com/router/rest jd: app-key: ${JD_APP_KEY} secret: ${JD_SECRET} pid: ${JD_PID}适配器通过ConfigurationProperties注入配置ConfigurationProperties(prefix union.taobao) Data public class TaobaoConfig { private String appKey; private String appSecret; private String apiUrl; } Component public class TaobaoUnionApiAdapter { private final TaobaoConfig config; public TaobaoUnionApiAdapter(TaobaoConfig config) { this.config config; } }7. 测试策略为了确保各平台适配器的稳定性我们设计了多层测试单元测试针对每个适配器的方法Test public void testTaobaoQueryItem() { TaobaoUnionApiAdapter adapter new TaobaoUnionApiAdapter(config); UnionItem item adapter.queryItem(12345); assertNotNull(item); assertEquals(12345, item.getItemId()); }集成测试测试真实API调用SpringBootTest public class TaobaoApiIntegrationTest { Autowired private UnionApi taobaoApi; Test public void testRealApiCall() { UnionItem item taobaoApi.queryItem(真实商品ID); assertNotNull(item.getTitle()); } }契约测试确保各平台返回的数据都能正确转换为统一模型8. 缓存策略为了减少API调用次数我们增加了多级缓存本地缓存使用Caffeine缓存常用商品Component public class CachedUnionApi implements UnionApi { private final UnionApi delegate; private final CacheString, UnionItem itemCache; public UnionItem queryItem(String itemId) { return itemCache.get(itemId, id - delegate.queryItem(id)); } }分布式缓存使用Redis缓存推广链接public UnionLink generatePromotionLink(String itemId, String adzoneId) { String cacheKey promo: platform : itemId : adzoneId; return redisTemplate.opsForValue().get(cacheKey, () - delegate.generatePromotionLink(itemId, adzoneId), 1, TimeUnit.HOURS); }9. 限流与降级为了防止API调用过量或被限流我们实现了令牌桶限流使用Resilience4jRateLimiterConfig config RateLimiterConfig.custom() .limitRefreshPeriod(Duration.ofSeconds(1)) .limitForPeriod(10) // 每秒10次 .build(); RateLimiter limiter RateLimiter.of(taobao-api, config); public UnionItem queryItem(String itemId) { return limiter.executeSupplier(() - { // 实际API调用 }); }熔断降级当API不可用时返回缓存数据或默认值CircuitBreaker breaker CircuitBreaker.ofDefaults(taobao-api); public UnionItem queryItem(String itemId) { return breaker.executeSupplier(() - { try { return delegate.queryItem(itemId); } catch (Exception e) { return getCachedItem(itemId); // 降级逻辑 } }); }10. 总结与展望这套基于适配器模式的多联盟API对接方案在我们系统中已经稳定运行两年多对接了8个主流电商平台。最大的优势是业务代码与平台解耦新增平台只需添加适配器不改动业务逻辑统一异常处理所有平台异常转换为统一格式便于扩展功能如缓存、限流等可以统一添加降低维护成本平台API变更只需修改对应适配器未来计划加入智能路由功能根据各平台的接口稳定性、响应时间等指标动态选择最优平台进行查询。同时探索使用GraphQL构建统一的API网关进一步简化客户端调用。