引言:多渠道的挑战

跨境电商通常在多个平台销售:

  • 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 差异对比

维度AmazoneBayShopify
订单号格式111-xxx-xxx12-xxx-xxx数字ID
时间格式ISO 8601ISO 8601ISO 8601
金额格式对象对象字符串
地址结构扁平嵌套扁平
状态定义自定义自定义自定义
API方式RESTRESTREST/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 核心要点

  1. 统一订单模型:设计通用的订单结构,覆盖各渠道字段
  2. 适配器模式:每个渠道一个适配器,负责格式转换
  3. 混合同步:定时拉取 + Webhook,保证不遗漏
  4. 幂等处理:根据渠道订单号去重
  5. 状态回传:发货后同步到渠道,支持重试

7.2 扩展新渠道

添加新渠道只需要:

  1. 实现ChannelAdapter接口
  2. 添加状态映射
  3. 配置店铺信息

系列文章导航

本文是《跨境电商数字化转型指南》系列的第14篇

  • 01-13. 前序文章
  • 14. 多渠道订单接入(本文)
  • 15. 订单拆分与合并策略