引言:多渠道的挑战
跨境电商通常在多个平台销售:
- Amazon(美国、欧洲、日本)
- eBay
- Shopify独立站
- 速卖通
- Wish
- TikTok Shop
每个渠道的订单格式和API都不同,如果不做统一处理:
- 每个渠道单独处理,代码重复
- 下游系统(WMS、TMS)要对接多种格式
- 数据分析困难
解决方案:设计统一的订单模型,通过适配器模式对接各渠道。
一、各渠道订单差异分析
1.1 Amazon订单特点
订单结构:
{
"AmazonOrderId": "111-1234567-1234567",
"PurchaseDate": "2024-01-29T10:00:00Z",
"OrderStatus": "Unshipped",
"FulfillmentChannel": "MFN",
"SalesChannel": "Amazon.com",
"OrderTotal": {
"CurrencyCode": "USD",
"Amount": "99.99"
},
"ShippingAddress": {
"Name": "John Doe",
"AddressLine1": "123 Main St",
"City": "Seattle",
"StateOrRegion": "WA",
"PostalCode": "98101",
"CountryCode": "US"
}
}
特殊点:
- FBA/FBM区分
- 多站点(US、UK、DE、JP等)
- 订单状态较多
- 有Prime标识
1.2 eBay订单特点
订单结构:
{
"orderId": "12-12345-12345",
"creationDate": "2024-01-29T10:00:00.000Z",
"orderFulfillmentStatus": "NOT_STARTED",
"pricingSummary": {
"total": {
"value": "99.99",
"currency": "USD"
}
},
"fulfillmentStartInstructions": [{
"shippingStep": {
"shipTo": {
"fullName": "John Doe",
"contactAddress": {
"addressLine1": "123 Main St",
"city": "Seattle",
"stateOrProvince": "WA",
"postalCode": "98101",
"countryCode": "US"
}
}
}
}]
}
特殊点:
- 支持拍卖模式
- 买家可以合并付款
- 有Best Offer功能
1.3 Shopify订单特点
订单结构:
{
"id": 1234567890,
"order_number": 1001,
"created_at": "2024-01-29T10:00:00-08:00",
"financial_status": "paid",
"fulfillment_status": null,
"total_price": "99.99",
"currency": "USD",
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "Seattle",
"province": "Washington",
"zip": "98101",
"country_code": "US"
}
}
特殊点:
- 支持Webhook推送
- 订单结构相对简单
- 可以自定义字段
1.4 差异对比
| 维度 | Amazon | eBay | Shopify |
|---|---|---|---|
| 订单号格式 | 111-xxx-xxx | 12-xxx-xxx | 数字ID |
| 时间格式 | ISO 8601 | ISO 8601 | ISO 8601 |
| 金额格式 | 对象 | 对象 | 字符串 |
| 地址结构 | 扁平 | 嵌套 | 扁平 |
| 状态定义 | 自定义 | 自定义 | 自定义 |
| API方式 | REST | REST | REST/GraphQL |
二、统一订单模型设计
2.1 核心实体
/**
* 统一订单模型
*/
@Data
public class Order {
// 基本信息
private String orderId; // 内部订单号
private String channelOrderId; // 渠道订单号
private String channel; // 渠道:AMAZON/EBAY/SHOPIFY
private String channelSite; // 站点:US/UK/DE
private String shopId; // 店铺ID
// 买家信息
private String buyerId; // 买家ID
private String buyerEmail; // 买家邮箱
// 收货地址
private Address shippingAddress;
// 订单明细
private List<OrderItem> items;
// 金额信息
private Money totalAmount; // 订单总额
private Money shippingFee; // 运费
private Money discount; // 折扣
private String currency; // 币种
// 状态信息
private OrderStatus status; // 订单状态
private PaymentStatus paymentStatus; // 支付状态
// 时间信息
private LocalDateTime orderTime; // 下单时间
private LocalDateTime payTime; // 支付时间
// 履约信息
private String warehouseId; // 分配仓库
private String carrierId; // 承运商
private String trackingNo; // 物流单号
// 扩展信息
private Map<String, String> extendInfo; // 渠道特有信息
}
/**
* 订单明细
*/
@Data
public class OrderItem {
private String itemId; // 明细ID
private String channelItemId; // 渠道明细ID
private String skuId; // SKU编码
private String channelSkuId; // 渠道SKU编码
private String skuName; // SKU名称
private Integer quantity; // 数量
private Money unitPrice; // 单价
private Money totalPrice; // 总价
}
/**
* 地址
*/
@Data
public class Address {
private String receiverName; // 收件人
private String phone; // 电话
private String country; // 国家
private String countryCode; // 国家代码
private String province; // 省/州
private String city; // 城市
private String district; // 区县
private String addressLine1; // 地址行1
private String addressLine2; // 地址行2
private String postalCode; // 邮编
}
/**
* 金额(支持多币种)
*/
@Data
public class Money {
private BigDecimal amount; // 金额
private String currency; // 币种
}
2.2 状态枚举
/**
* 统一订单状态
*/
public enum OrderStatus {
PENDING_PAYMENT, // 待支付
PENDING_REVIEW, // 待审核
PENDING_FULFILL, // 待履约
FULFILLING, // 履约中
SHIPPED, // 已发货
DELIVERED, // 已签收
COMPLETED, // 已完成
CANCELLED, // 已取消
REFUNDING, // 退款中
REFUNDED // 已退款
}
/**
* 渠道状态映射
*/
public class OrderStatusMapper {
// Amazon状态映射
private static final Map<String, OrderStatus> AMAZON_STATUS_MAP = Map.of(
"Pending", OrderStatus.PENDING_PAYMENT,
"Unshipped", OrderStatus.PENDING_FULFILL,
"PartiallyShipped", OrderStatus.FULFILLING,
"Shipped", OrderStatus.SHIPPED,
"Canceled", OrderStatus.CANCELLED
);
// eBay状态映射
private static final Map<String, OrderStatus> EBAY_STATUS_MAP = Map.of(
"NOT_STARTED", OrderStatus.PENDING_FULFILL,
"IN_PROGRESS", OrderStatus.FULFILLING,
"FULFILLED", OrderStatus.SHIPPED
);
public static OrderStatus fromAmazon(String amazonStatus) {
return AMAZON_STATUS_MAP.getOrDefault(amazonStatus, OrderStatus.PENDING_REVIEW);
}
public static OrderStatus fromEbay(String ebayStatus) {
return EBAY_STATUS_MAP.getOrDefault(ebayStatus, OrderStatus.PENDING_REVIEW);
}
}
三、适配器架构设计
3.1 架构图
┌─────────────────────────────────────────────────────┐
│ 订单服务 │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ 适配器管理器 │
│ AdapterManager │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌───▼───┐ ┌────▼────┐ ┌────▼────┐
│Amazon │ │ eBay │ │Shopify │
│Adapter│ │ Adapter │ │Adapter │
└───┬───┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
Amazon API eBay API Shopify API
3.2 适配器接口
/**
* 渠道适配器接口
*/
public interface ChannelAdapter {
/**
* 获取渠道标识
*/
String getChannel();
/**
* 拉取订单
* @param shopId 店铺ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 订单列表
*/
List<Order> pullOrders(String shopId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 获取单个订单
* @param shopId 店铺ID
* @param channelOrderId 渠道订单号
* @return 订单
*/
Order getOrder(String shopId, String channelOrderId);
/**
* 回传发货信息
* @param shopId 店铺ID
* @param channelOrderId 渠道订单号
* @param trackingNo 物流单号
* @param carrier 承运商
*/
void shipOrder(String shopId, String channelOrderId, String trackingNo, String carrier);
/**
* 取消订单
*/
void cancelOrder(String shopId, String channelOrderId, String reason);
}
3.3 Amazon适配器实现
@Component
@Slf4j
public class AmazonAdapter implements ChannelAdapter {
@Autowired
private AmazonApiClient amazonApiClient;
@Autowired
private ShopConfigService shopConfigService;
@Override
public String getChannel() {
return "AMAZON";
}
@Override
public List<Order> pullOrders(String shopId, LocalDateTime startTime, LocalDateTime endTime) {
// 1. 获取店铺配置
ShopConfig config = shopConfigService.getConfig(shopId);
// 2. 调用Amazon API
List<AmazonOrder> amazonOrders = amazonApiClient.getOrders(
config.getAccessKey(),
config.getSecretKey(),
config.getMarketplaceId(),
startTime,
endTime
);
// 3. 转换为统一格式
return amazonOrders.stream()
.map(this::convert)
.collect(Collectors.toList());
}
@Override
public Order getOrder(String shopId, String channelOrderId) {
ShopConfig config = shopConfigService.getConfig(shopId);
AmazonOrder amazonOrder = amazonApiClient.getOrder(
config.getAccessKey(),
config.getSecretKey(),
channelOrderId
);
return convert(amazonOrder);
}
@Override
public void shipOrder(String shopId, String channelOrderId, String trackingNo, String carrier) {
ShopConfig config = shopConfigService.getConfig(shopId);
amazonApiClient.confirmShipment(
config.getAccessKey(),
config.getSecretKey(),
channelOrderId,
trackingNo,
mapCarrier(carrier)
);
}
/**
* Amazon订单转换为统一订单
*/
private Order convert(AmazonOrder amazonOrder) {
Order order = new Order();
// 基本信息
order.setChannelOrderId(amazonOrder.getAmazonOrderId());
order.setChannel("AMAZON");
order.setChannelSite(extractSite(amazonOrder.getSalesChannel()));
// 金额
if (amazonOrder.getOrderTotal() != null) {
order.setTotalAmount(new Money(
new BigDecimal(amazonOrder.getOrderTotal().getAmount()),
amazonOrder.getOrderTotal().getCurrencyCode()
));
order.setCurrency(amazonOrder.getOrderTotal().getCurrencyCode());
}
// 地址
if (amazonOrder.getShippingAddress() != null) {
Address address = new Address();
address.setReceiverName(amazonOrder.getShippingAddress().getName());
address.setAddressLine1(amazonOrder.getShippingAddress().getAddressLine1());
address.setAddressLine2(amazonOrder.getShippingAddress().getAddressLine2());
address.setCity(amazonOrder.getShippingAddress().getCity());
address.setProvince(amazonOrder.getShippingAddress().getStateOrRegion());
address.setPostalCode(amazonOrder.getShippingAddress().getPostalCode());
address.setCountryCode(amazonOrder.getShippingAddress().getCountryCode());
order.setShippingAddress(address);
}
// 状态
order.setStatus(OrderStatusMapper.fromAmazon(amazonOrder.getOrderStatus()));
// 时间
order.setOrderTime(parseDateTime(amazonOrder.getPurchaseDate()));
// 扩展信息
Map<String, String> extendInfo = new HashMap<>();
extendInfo.put("fulfillmentChannel", amazonOrder.getFulfillmentChannel());
extendInfo.put("isPrime", String.valueOf(amazonOrder.getIsPrime()));
order.setExtendInfo(extendInfo);
return order;
}
private String extractSite(String salesChannel) {
// Amazon.com -> US, Amazon.co.uk -> UK
if (salesChannel.contains(".com")) return "US";
if (salesChannel.contains(".co.uk")) return "UK";
if (salesChannel.contains(".de")) return "DE";
if (salesChannel.contains(".co.jp")) return "JP";
return "US";
}
}
3.4 适配器管理器
@Component
public class AdapterManager {
private final Map<String, ChannelAdapter> adapterMap;
@Autowired
public AdapterManager(List<ChannelAdapter> adapters) {
this.adapterMap = adapters.stream()
.collect(Collectors.toMap(
ChannelAdapter::getChannel,
Function.identity()
));
}
public ChannelAdapter getAdapter(String channel) {
ChannelAdapter adapter = adapterMap.get(channel.toUpperCase());
if (adapter == null) {
throw new UnsupportedChannelException("Unsupported channel: " + channel);
}
return adapter;
}
public List<String> getSupportedChannels() {
return new ArrayList<>(adapterMap.keySet());
}
}
四、订单拉取策略
4.1 定时拉取
@Component
@Slf4j
public class OrderPullJob {
@Autowired
private AdapterManager adapterManager;
@Autowired
private ShopService shopService;
@Autowired
private OrderService orderService;
/**
* 每5分钟拉取一次订单
*/
@Scheduled(fixedRate = 5 * 60 * 1000)
public void pullOrders() {
List<Shop> shops = shopService.getActiveShops();
for (Shop shop : shops) {
try {
pullOrdersForShop(shop);
} catch (Exception e) {
log.error("拉取订单失败: shopId={}", shop.getShopId(), e);
}
}
}
private void pullOrdersForShop(Shop shop) {
ChannelAdapter adapter = adapterManager.getAdapter(shop.getChannel());
// 获取上次拉取时间
LocalDateTime lastPullTime = shop.getLastOrderPullTime();
if (lastPullTime == null) {
lastPullTime = LocalDateTime.now().minusDays(7);
}
LocalDateTime endTime = LocalDateTime.now();
// 拉取订单
List<Order> orders = adapter.pullOrders(shop.getShopId(), lastPullTime, endTime);
// 保存订单
for (Order order : orders) {
orderService.saveOrUpdate(order);
}
// 更新拉取时间
shopService.updateLastPullTime(shop.getShopId(), endTime);
log.info("拉取订单完成: shopId={}, count={}", shop.getShopId(), orders.size());
}
}
4.2 Webhook推送(Shopify)
@RestController
@RequestMapping("/webhook/shopify")
@Slf4j
public class ShopifyWebhookController {
@Autowired
private ShopifyAdapter shopifyAdapter;
@Autowired
private OrderService orderService;
@PostMapping("/orders/create")
public ResponseEntity<Void> onOrderCreate(
@RequestHeader("X-Shopify-Shop-Domain") String shopDomain,
@RequestHeader("X-Shopify-Hmac-SHA256") String hmac,
@RequestBody String payload) {
// 1. 验证签名
if (!verifyHmac(payload, hmac)) {
return ResponseEntity.status(401).build();
}
// 2. 解析订单
ShopifyOrder shopifyOrder = JSON.parseObject(payload, ShopifyOrder.class);
// 3. 转换并保存
Order order = shopifyAdapter.convert(shopifyOrder);
orderService.saveOrUpdate(order);
log.info("Shopify订单创建: orderId={}", order.getChannelOrderId());
return ResponseEntity.ok().build();
}
@PostMapping("/orders/updated")
public ResponseEntity<Void> onOrderUpdate(
@RequestHeader("X-Shopify-Shop-Domain") String shopDomain,
@RequestHeader("X-Shopify-Hmac-SHA256") String hmac,
@RequestBody String payload) {
// 类似处理
return ResponseEntity.ok().build();
}
}
4.3 混合模式
/**
* 订单同步服务
* 支持定时拉取 + Webhook推送的混合模式
*/
@Service
public class OrderSyncService {
/**
* 定时拉取(兜底)
* 即使有Webhook,也定时拉取,防止遗漏
*/
@Scheduled(fixedRate = 30 * 60 * 1000) // 30分钟
public void scheduledPull() {
// 拉取逻辑
}
/**
* Webhook接收(实时)
*/
public void onWebhook(String channel, String payload) {
// Webhook处理逻辑
}
/**
* 手动同步(补单)
*/
public void manualSync(String shopId, String channelOrderId) {
// 手动拉取单个订单
}
}
五、幂等处理
5.1 为什么需要幂等
- 定时拉取可能重复拉取同一订单
- Webhook可能重复推送
- 网络重试可能导致重复
5.2 幂等实现
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
/**
* 保存或更新订单(幂等)
*/
@Transactional
public Order saveOrUpdate(Order order) {
// 1. 根据渠道订单号查询
Order existingOrder = orderRepository.findByChannelOrderId(
order.getChannel(),
order.getChannelOrderId()
);
if (existingOrder == null) {
// 2. 新订单:生成内部订单号,保存
order.setOrderId(generateOrderId());
return orderRepository.save(order);
} else {
// 3. 已存在:更新(只更新允许更新的字段)
updateOrder(existingOrder, order);
return orderRepository.save(existingOrder);
}
}
private void updateOrder(Order existing, Order newOrder) {
// 只更新状态、物流信息等可变字段
existing.setStatus(newOrder.getStatus());
existing.setTrackingNo(newOrder.getTrackingNo());
// 不更新金额、地址等(以首次为准)
}
}
六、状态回传
6.1 发货状态回传
@Service
@Slf4j
public class ShipmentService {
@Autowired
private AdapterManager adapterManager;
@Autowired
private OrderRepository orderRepository;
/**
* 发货并回传状态
*/
@Transactional
public void ship(String orderId, String trackingNo, String carrier) {
Order order = orderRepository.findByOrderId(orderId);
// 1. 更新本地状态
order.setStatus(OrderStatus.SHIPPED);
order.setTrackingNo(trackingNo);
order.setCarrierId(carrier);
orderRepository.save(order);
// 2. 回传渠道
try {
ChannelAdapter adapter = adapterManager.getAdapter(order.getChannel());
adapter.shipOrder(
order.getShopId(),
order.getChannelOrderId(),
trackingNo,
carrier
);
log.info("发货状态回传成功: orderId={}", orderId);
} catch (Exception e) {
log.error("发货状态回传失败: orderId={}", orderId, e);
// 记录失败,后续重试
saveRetryTask(order, "SHIP", trackingNo, carrier);
}
}
}
6.2 重试机制
@Component
public class ChannelSyncRetryJob {
@Autowired
private RetryTaskRepository retryTaskRepository;
@Autowired
private ShipmentService shipmentService;
/**
* 每10分钟重试失败的任务
*/
@Scheduled(fixedRate = 10 * 60 * 1000)
public void retryFailedTasks() {
List<RetryTask> tasks = retryTaskRepository.findPendingTasks(10);
for (RetryTask task : tasks) {
try {
if ("SHIP".equals(task.getTaskType())) {
// 重试发货回传
shipmentService.retryShip(task);
}
task.setStatus("SUCCESS");
} catch (Exception e) {
task.setRetryCount(task.getRetryCount() + 1);
if (task.getRetryCount() >= 5) {
task.setStatus("FAILED");
// 告警
}
}
retryTaskRepository.save(task);
}
}
}
七、总结
7.1 核心要点
- 统一订单模型:设计通用的订单结构,覆盖各渠道字段
- 适配器模式:每个渠道一个适配器,负责格式转换
- 混合同步:定时拉取 + Webhook,保证不遗漏
- 幂等处理:根据渠道订单号去重
- 状态回传:发货后同步到渠道,支持重试
7.2 扩展新渠道
添加新渠道只需要:
- 实现
ChannelAdapter接口 - 添加状态映射
- 配置店铺信息
系列文章导航
本文是《跨境电商数字化转型指南》系列的第14篇
- 01-13. 前序文章
- 14. 多渠道订单接入(本文)
- 15. 订单拆分与合并策略