> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 自定义集成

> 直接调用 Strategies API 来驱动自定义店面体验——PDP 轮播、自定义购物车追加销售，以及 Aftersell 原生界面覆盖不到的任何地方。

<div id="overview">
  ## 概述
</div>

当 Aftersell 的原生界面（购买后、结账、Upcart）和现成的打包集成都不适用时，你可以从 Shopify 主题中自行调用 Strategies API，并以任意方式渲染返回的商品。

各种情况下的模式都相同：用 Liquid 构建上下文负载（这样当前商品、购物车内容和客户字段等 Shopify 属性会在渲染时被填充），将其 `POST` 到 `/api/public/strategy/evaluate`，然后渲染响应。

本页介绍两种实现模式：

* **PDP 上下文**——在商品页面放置一个 section，以**当前浏览的商品**调用 API，并以轮播形式渲染返回的推荐商品。
* **购物车上下文**——在自定义购物车内渲染一个追加销售区块，以**当前购物车的所有行项目**调用 API，并渲染返回的商品。

两者的区别在于**商品上下文**的形态：PDP 上是单个商品，购物车中是所有行项目组成的数组。

***

<div id="what-youll-need">
  ## 前置条件
</div>

1. \*\*你的 Strategy API 密钥。\*\*在 Aftersell 中，前往 **Settings → Product Strategy**，在 **Security Token** 卡片中复制你的令牌（这就是你的 Strategy API 密钥）。
2. \*\*Strategy ID。\*\*在 Aftersell Strategy 编辑器中打开你要运行的 Strategy 并复制其 ID。
3. \*\*主题代码访问权限。\*\*你将向 Shopify 主题中添加一个 Liquid section（PDP）或 block（自定义购物车）——Online Store → Themes → ... → Edit code。

<Warning>
  你的 Strategy API 密钥位于客户端主题代码中，任何查看页面源代码的人都能看到它。请将其视为公开的店面凭证，如果它以你意料之外的方式暴露，请在 Aftersell 的 **Settings → Product Strategy** 中轮换它。
</Warning>

***

<div id="pdp-context-section-snippet">
  ## PDP 上下文：Section 代码片段
</div>

此模式向你的商品页面添加一个 Shopify section。页面渲染时，Liquid 将当前商品、购物车和客户属性嵌入负载，然后 JavaScript 向 Strategies API 发送 POST 请求，并在 Splide 轮播中渲染返回的商品。

<div id="installing">
  ### 安装
</div>

1. 在 Shopify 后台，前往 **Online Store → Themes**，点击主题上的 **...**，选择 **Edit code**。
2. 在 **Sections** 文件夹下，创建一个名为 `aftersell-upsell-carousel.liquid` 的新文件。
3. 将下面的代码片段粘贴到新文件中，并将 `YOUR_STRATEGY_API_KEY` 替换为来自 Aftersell 的 API 密钥。
4. 保存。
5. 打开你的商品模板（通常是 `templates/product.json` 或 `sections/main-product.liquid`），在你希望轮播出现的位置添加 **Aftersell Carousel** section。你也可以在主题编辑器中直接将它拖到商品页面上。
6. 在该 section 的设置中，粘贴你的 **Strategy ID**。

<div id="what-the-section-sends">
  ### 该 section 发送的内容
</div>

对每次 PDP 浏览，负载包括：

* **products**——一个只含一个元素的数组，包含当前浏览的商品（productId、variantId、quantity、price、handle、title、vendor、productType、tags、collections、sellingPlan）。
* **cart**——顾客当前购物车的小计、商品件数、行数（购物车为空时省略）。
* **cartToken**——使 API 能将此次评估拼接到同一会话中。
* **customer**——标签、国家、省份、语言区域、订单数、总消费额和接受营销标记，但**仅当顾客已登录时**。
* **session**——来自 `shop.currency` 的货币代码。

该 section 默认不发送 UTM 参数。如果你希望在 PDP 上进行基于 UTM 的定向，请在客户端捕获它们，并在 fetch 之前将它们添加到 `session` 对象中。

<div id="the-snippet">
  ### 代码片段
</div>

```liquid theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{% comment %}
  Aftersell Carousel (Splide)
  Type: Section — save to sections/aftersell-upsell-carousel.liquid
{% endcomment %}

{% if product %}

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide-core.min.css">
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js" defer></script>

<div id="aftersell-upsell-{{ section.id }}" class="aftersell-upsell-carousel" style="display:none" {{ section.shopify_attributes }}>
  <h2 class="aftersell-heading">{{ section.settings.heading | default: 'You might also like' }}</h2>
  <div class="aftersell-carousel-wrapper">
    <button class="aftersell-arrow aftersell-arrow--prev" aria-label="Previous" disabled>&#8592;</button>
    <div class="aftersell-track-container">
      <div class="splide" id="aftersell-splide-{{ section.id }}">
        <div class="splide__track">
          <ul class="splide__list">
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
          </ul>
        </div>
      </div>
    </div>
    <button class="aftersell-arrow aftersell-arrow--next" aria-label="Next" disabled>&#8594;</button>
  </div>
</div>

<style>
.aftersell-upsell-carousel { font-family: Modernist, sans-serif; max-width: 1300px; margin: 0 auto; padding: 24px 0 0; box-sizing: border-box; }
.aftersell-heading { font-family: Modernist, sans-serif; font-size: 30px; font-weight: 700; line-height: 45px; color: #0C0A09; margin: 0; }
.aftersell-carousel-wrapper { display: flex; align-items: center; gap: 8px; }
.aftersell-track-container { overflow: hidden; flex: 1; min-width: 0; }
.aftersell-card { display: flex; flex-direction: column; box-sizing: border-box; height: 100%; }
.aftersell-card-link { text-decoration: none; color: inherit; display: block; flex: 1; }
.aftersell-card-image { aspect-ratio: 1; overflow: hidden; background: #f5f5f5; border-radius: 8px 8px 0 0; position: relative; }
.aftersell-card-image img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.3s ease; }
.aftersell-card-image:hover img { transform: scale(1.04); }
.aftersell-card-badge { position: absolute; top: 10px; left: 8px; background: #c60006; color: #fff; font-size: 11px; font-weight: 500; padding: 4px 8px; border-radius: 16px; z-index: 1; }
.aftersell-card-body { padding: 6px 0 0; display: flex; flex-direction: column; }
.aftersell-card-vendor { font-size: 13px; font-weight: 550; text-transform: uppercase; color: #1d4481; margin: 0; }
.aftersell-card-title { font-size: 13px; font-weight: 700; color: #0C0A09; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aftersell-card-price { font-size: 13px; color: #0C0A09; margin: 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.aftersell-card-price--compare { text-decoration: line-through; color: #595959; }
.aftersell-card-price--sale { font-weight: 700; color: #c60006; }
.aftersell-card-actions { padding: 6px 0 0; display: flex; flex-direction: column; margin-top: auto; }
.aftersell-variant-select { width: 100%; font-size: 13px; padding: 0.35rem 0.5rem; border: 1px solid #DBDBDB; border-radius: 4px; background: #fff; color: #0C0A09; cursor: pointer; }
.aftersell-cta { padding: 16px 12px; background: #c50007; color: #FFFFFF; border: none; border-radius: 5px; font-size: 16px; font-weight: 700; cursor: pointer; width: 100%; display: flex; align-items: center; justify-content: center; transition: background 0.2s, opacity 0.2s; }
.aftersell-cta:hover:not(:disabled) { opacity: 0.85; }
.aftersell-cta:disabled { opacity: 0.5; cursor: default; }
.aftersell-cta--added { background: #1d4481; }
.aftersell-cta--unavailable { background: #999; cursor: default; }
.aftersell-arrow { width: 30px; height: 90px; background: rgba(255,255,255,0.6); border: 1px solid #DBDBDB; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0; }
.aftersell-arrow:disabled { opacity: 0.3; cursor: default; }
.aftersell-skeleton { border-radius: 8px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: aftersell-shimmer 1.4s infinite; aspect-ratio: 0.75; width: 100%; }
@keyframes aftersell-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

#aftersell-splide-{{ section.id }} .splide__arrows,
#aftersell-splide-{{ section.id }} .splide__pagination { display: none !important; }

@media (max-width: 768px) {
  .aftersell-upsell-carousel { padding: 16px 16px 0; }
  .aftersell-arrow { display: none; }
  .aftersell-carousel-wrapper { gap: 0; }
}
</style>

<script>
(function() {
  var BACKEND_URL  = 'https://start.aftersell.app';
  var API_KEY      = 'YOUR_STRATEGY_API_KEY';
  var STRATEGY_ID  = {{ section.settings.strategy_id | json }};
  var SHOP_DOMAIN  = {{ shop.permanent_domain | json }};
  var CTA_LABEL    = '{{ section.settings.cta_label | default: "Add to cart" }}';
  var MAX_PRODUCTS = {{ section.settings.max_products | default: 8 }};
  var CURRENCY     = {{ shop.currency | default: "USD" | json }};
  var SECTION_ID   = {{ section.id | json }};
  if (!SHOP_DOMAIN || !STRATEGY_ID) return;

  var fmt;
  try {
    fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: CURRENCY });
  } catch(e) {
    fmt = { format: function(n) { return '$' + parseFloat(n).toFixed(2); } };
  }
  function money(str) { return fmt.format(parseFloat(str) || 0); }

  function gidToNumeric(gid) {
    return gid ? String(gid).split('/').pop() : null;
  }

  function priceHtml(price, compareAt) {
    var hasSale = compareAt && parseFloat(compareAt) > parseFloat(price);
    return hasSale
      ? '<span class="aftersell-card-price--compare">' + money(compareAt) + '</span>'
        + '<span class="aftersell-card-price--sale">' + money(price) + '</span>'
      : '<span class="aftersell-card-price--current">' + money(price) + '</span>';
  }

  var productContext = {
    productId:   'gid://shopify/Product/{{ product.id }}',
    variantId:   'gid://shopify/ProductVariant/{{ product.selected_or_first_available_variant.id }}',
    quantity:    1,
    price:       {{ product.price | divided_by: 100.0 }},
    handle:      {{ product.handle | json }},
    title:       {{ product.title | json }},
    vendor:      {{ product.vendor | json }},
    productType: {{ product.type | json }},
    tags:        {{ product.tags | json }},
    collections: [{% for col in product.collections %}'gid://shopify/Collection/{{ col.id }}'{% unless forloop.last %},{% endunless %}{% endfor %}],
    sellingPlan: {% if product.selected_selling_plan %}'subscription'{% else %}'one-time'{% endif %}
  };

  var cartContext = {
    subtotal:  {{ cart.total_price | divided_by: 100.0 }},
    itemCount: {{ cart.item_count }},
    lineCount: {{ cart.items.size }}
  };

  {% if customer %}
  var customerContext = {
    customerId:       'gid://shopify/Customer/{{ customer.id }}',
    tags:             {{ customer.tags | json }},
    {% if customer.default_address.country_code %}countryCode: {{ customer.default_address.country_code | json }},{% endif %}
    {% if customer.default_address.province_code %}provinceCode: {{ customer.default_address.province_code | json }},{% endif %}
    locale:           {{ request.locale.iso_code | json }},
    orderCount:       {{ customer.orders_count }},
    totalSpent:       {{ customer.total_spent | times: 1.0 }},
    acceptsMarketing: {{ customer.accepts_marketing }}
  };
  {% endif %}

  var blockEl  = document.getElementById('aftersell-upsell-' + SECTION_ID);
  var splideEl = document.getElementById('aftersell-splide-' + SECTION_ID);
  var list     = splideEl.querySelector('.splide__list');
  var prevBtn  = blockEl.querySelector('.aftersell-arrow--prev');
  var nextBtn  = blockEl.querySelector('.aftersell-arrow--next');
  var splideInstance = null;

  function getCartToken() {
    var t = {{ cart.token | json }};
    if (t) return Promise.resolve(t);
    return fetch('/cart.js').then(function(r){ return r.json(); }).then(function(c){ return c.token || null; }).catch(function(){ return null; });
  }

  function evaluate() {
    getCartToken().then(function(cartToken) {
      if (!cartToken) return;
      return fetch(BACKEND_URL + '/api/public/strategy/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Strategy-Api-Key': API_KEY },
        body: JSON.stringify({
          shopDomain:  SHOP_DOMAIN,
          strategyId:  STRATEGY_ID,
          context: {
            products:  [productContext],
            cartToken: cartToken,
            cart:      cartContext.itemCount > 0 ? cartContext : undefined,
            {% if customer %}customer: customerContext,{% endif %}
            session:   { currencyCode: CURRENCY }
          }
        })
      });
    }).then(function(res) { if (!res) return; return res.json(); })
    .then(function(data) {
      if (!data || !data.success || !data.products || !data.products.length) {
        clearSkeletons(); return;
      }
      renderCards(data.products.slice(0, MAX_PRODUCTS));
    }).catch(function(err) {
      console.error('[AfterSell carousel] Error:', err);
      clearSkeletons();
    });
  }

  function renderCards(products) {
    if (!products || !products.length) {
      blockEl.style.display = 'none'; return;
    }

    blockEl.style.display = '';

    list.innerHTML = products.map(function(p, i) {
      var img      = p.images && p.images[0] ? p.images[0].src : '';
      var alt      = (p.images && p.images[0] && p.images[0].altText) || p.title;
      var variants = p.variants || [];
      var availableVariants = variants.filter(function(v) {
        return v.availableForSale !== false;
      });
      var isDefaultVariant = variants.length === 1 && variants[0].title === 'Default Title';
      var showSelect       = !isDefaultVariant && availableVariants.length >= 1;
      var firstVariant     = availableVariants.length ? availableVariants[0] : null;
      var firstVariantId   = firstVariant ? gidToNumeric(firstVariant.variantId) : null;
      var anyAvailable     = availableVariants.length > 0;

      var onSale = p.compareAtPrice && parseFloat(p.compareAtPrice) > parseFloat(p.price);
      var badge  = '';
      if (onSale) {
        var pct = Math.round((1 - parseFloat(p.price) / parseFloat(p.compareAtPrice)) * 100);
        badge = '<span class="aftersell-card-badge">Save ' + pct + '%</span>';
      }
      var vendor = p.vendor ? '<p class="aftersell-card-vendor">' + p.vendor + '</p>' : '';

      var selectHtml = '';
      if (showSelect) {
        var options = availableVariants.map(function(v) {
          return '<option value="' + gidToNumeric(v.variantId) + '"'
            + ' data-price="'   + (v.price         || p.price) + '"'
            + ' data-compare="' + (v.compareAtPrice || '')     + '"'
            + '>' + v.title + '</option>';
        }).join('');
        selectHtml = '<select class="aftersell-variant-select" aria-label="Select variant">' + options + '</select>';
      }

      var atcLabel = anyAvailable ? CTA_LABEL : 'Sold Out';
      var atcClass = anyAvailable ? 'aftersell-cta' : 'aftersell-cta aftersell-cta--unavailable';
      var atcBtn   = '<button class="' + atcClass + '"'
        + (firstVariantId ? ' data-variant-id="' + firstVariantId + '"' : '')
        + (anyAvailable ? '' : ' disabled')
        + '>' + atcLabel + '</button>';

      var productUrl = p.url + (p.url.indexOf('?') > -1 ? '&' : '?') + 'ref=aftersell';

      return '<li class="splide__slide">'
        + '<div class="aftersell-card" data-idx="' + i + '">'
        + '<a class="aftersell-card-link" href="' + productUrl + '">'
        + '<div class="aftersell-card-image">' + badge
        + (img ? '<img src="' + img + '" alt="' + alt + '" loading="lazy">' : '')
        + '</div>'
        + '<div class="aftersell-card-body">'
        + vendor
        + '<p class="aftersell-card-title">' + p.title + '</p>'
        + '<p class="aftersell-card-price" data-price-el>' + priceHtml(p.price, p.compareAtPrice) + '</p>'
        + '</div></a>'
        + '<div class="aftersell-card-actions">'
        + selectHtml
        + atcBtn
        + '</div></div></li>';
    }).join('');

    attachCardEvents();
    initSplide();
  }

  function attachCardEvents() {
    list.addEventListener('change', function(e) {
      if (!e.target.classList.contains('aftersell-variant-select')) return;
      var select  = e.target;
      var card    = select.closest('.aftersell-card');
      var opt     = select.options[select.selectedIndex];
      var priceEl = card.querySelector('[data-price-el]');
      var btn     = card.querySelector('.aftersell-cta');
      priceEl.innerHTML     = priceHtml(opt.getAttribute('data-price'), opt.getAttribute('data-compare'));
      btn.dataset.variantId = opt.value;
      btn.disabled          = false;
      btn.textContent       = CTA_LABEL;
      btn.className         = 'aftersell-cta';
    });

    list.addEventListener('click', function(e) {
      var btn = e.target.closest('.aftersell-cta');
      if (!btn || btn.disabled) return;
      var variantId = btn.dataset.variantId;
      if (!variantId) return;

      e.preventDefault();
      btn.disabled    = true;
      btn.textContent = 'Adding…';

      fetch('/cart/add.js', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: parseInt(variantId, 10),
          quantity: 1,
          properties: {
            "_source": "Aftersell",
            "_attribution": "CTA"
          }
        })
      })
      .then(function(r) {
        if (!r.ok) throw new Error('Cart add failed: ' + r.status);
        return r.json();
      })
      .then(function() {
        btn.textContent = 'Added!';
        btn.classList.add('aftersell-cta--added');
        if (typeof window.theme !== 'undefined' && window.theme.cart && window.theme.cart.open) {
          window.theme.cart.open();
        }
        setTimeout(function() {
          btn.textContent = CTA_LABEL;
          btn.classList.remove('aftersell-cta--added');
          btn.disabled = false;
        }, 2500);
      })
      .catch(function(err) {
        console.error('[AfterSell carousel] Add to cart error:', err);
        btn.textContent = 'Try Again';
        btn.disabled    = false;
      });
    });
  }

  function initSplide() {
    function mount() {
      if (typeof window.Splide === 'undefined') {
        setTimeout(mount, 50);
        return;
      }

      splideInstance = new Splide('#aftersell-splide-' + SECTION_ID, {
        type:       'slide',
        perPage:    4,
        perMove:    4,
        gap:        '10px',
        pagination: false,
        arrows:     false,
        speed:      350,
        drag:       false,
        breakpoints: {
          768: { perPage: 2, perMove: 2, drag: 'free', snap: true },
          480: { perPage: 1, perMove: 1, drag: 'free', snap: true, padding: { right: '25%' } }
        }
      }).mount();

      function updateArrows() {
        var idx = splideInstance.index;
        var end = splideInstance.length - splideInstance.options.perPage;
        prevBtn.disabled = idx <= 0;
        nextBtn.disabled = idx >= end;
      }

      prevBtn.addEventListener('click', function() { splideInstance.go('<'); });
      nextBtn.addEventListener('click', function() { splideInstance.go('>'); });
      splideInstance.on('moved', updateArrows);
      updateArrows();
    }
    mount();
  }

  function clearSkeletons() { list.innerHTML = ''; }

  evaluate();
})();
</script>

{% endif %}

{% schema %}
{
  "name": "Aftersell Carousel",
  "settings": [
    { "type": "text",  "id": "strategy_id",  "label": "AfterSell Strategy ID", "default": "ADD_ID_HERE" },
    { "type": "text",  "id": "heading",      "label": "Heading",               "default": "You might also like" },
    { "type": "text",  "id": "cta_label",    "label": "CTA Button Label",      "default": "Add to cart" },
    { "type": "range", "id": "max_products", "label": "Max Products to Show",  "default": 8, "min": 1, "max": 20, "step": 1 }
  ],
  "presets": [{ "name": "Aftersell Carousel" }]
}
{% endschema %}
```

<Frame>
  <img src="https://mintcdn.com/aftersell/SnVX3h-PpMMxQMDU/images/aftersell/strategy-carousel-example.png?fit=max&auto=format&n=SnVX3h-PpMMxQMDU&q=85&s=8fb7ac08c2ae0c430b5bc4f810c59e40" alt="在 Shopify 商品页面上渲染的由 Strategy 驱动的商品轮播" width="2692" height="1146" data-path="images/aftersell/strategy-carousel-example.png" />
</Frame>

<div id="customizing">
  ### 自定义
</div>

该 section 的 schema 提供了四个商家可编辑的设置：**Strategy ID**、**Heading**、**CTA Button Label** 和 **Max Products to Show**。在 `{% schema %}` 块中添加或移除设置，即可向主题编辑器暴露更多选项。

CSS 限定在 `.aftersell-*` 类名下，包含一个由 Splide 驱动的 4 列轮播，在 768px 时降为 2 列，在 480px 时降为 1 列。你可以随意编辑以匹配你的主题——这些样式都不是 API 调用正常工作的必要条件。

***

<div id="cart-context-custom-cart-upsell-block">
  ## 购物车上下文：自定义购物车追加销售区块
</div>

此模式在结构上与 PDP 模式相同，只有一个关键区别：**商品上下文数组**由购物车的行项目构建，而不是当前浏览的商品。Strategy 会接收顾客添加的每个商品，并基于整个购物车返回推荐。

具体实现位于你的自定义购物车代码所在的位置——渲染购物车抽屉的 Liquid section、无头店面中的自定义区块，或 `cart.liquid` 之类的主题模板。API 调用的形式和响应处理与 PDP 示例完全相同——只有 `products` 数组不同。

结构如下：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
var products = {{ cart.items | json }}.map(function(item) {
  return {
    productId: 'gid://shopify/Product/' + item.product_id,
    variantId: 'gid://shopify/ProductVariant/' + item.variant_id,
    quantity:  item.quantity,
    price:     item.price / 100
    // ...other fields as needed
  };
});
```

负载的其余部分（cart、customer、session、cartToken）以及对 `/api/public/strategy/evaluate` 的 `fetch` 调用与上文的 PDP 模式相同——只有 `products` 数组从 `[productContext]` 换成了由购物车派生的数组。

***

<div id="what-happens-when-the-strategy-returns">
  ## Strategy 返回后会发生什么
</div>

无论你发送的是哪种上下文，响应形式都相同：

```json theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{
  "success": true,
  "products": [ /* enriched recommended products */ ],
  "evaluationId": "f3a1c2e0-...",
  "resolution": { "strategyId": "...", "matchedRuleIds": [...], "fallbackUsed": false },
  "meta": { "servedFromCache": false, "processingTimeMs": 12, "data": {} }
}
```

`evaluationId` 是本次评估的唯一 ID。如果你捕获它并将其附加到所渲染的商品上，就可以将最终产生的订单归因到产生它的确切推荐——见下文的[归因](#attribution)。

如何渲染 `products` 数组完全取决于你的主题代码。上面的 PDP 代码片段将它们渲染为带有变体选择器和加购按钮的卡片轮播；自定义购物车区块可以在抽屉内将它们渲染为垂直列表。

完整的请求和响应 schema 参见 [Evaluate Strategy API 参考](/zh/aftersell/strategies_api_reference_evaluate_strategy)。

***

<div id="when-no-product-is-returned">
  ## 未返回商品时
</div>

如果 Strategy 未返回任何商品（`products: []`），如何处理由你的代码决定。上面的 PDP 代码片段会完全隐藏轮播。自定义购物车区块可以回退到购物车的默认追加销售列表，或者干脆什么都不渲染。

为避免空响应，请在 Strategy 中配置 **Catch all**，这样始终会有一个可返回的兜底商品。有关如何设置 Catch all，请参见[构建 Strategies](/zh/aftersell/strategies_building_in_app) 页面。

***

<div id="tips-for-custom-integrations">
  ## 自定义集成小贴士
</div>

* \*\*用 Liquid 构建上下文。\*\*Liquid 在渲染时运行，可以访问完整的 Shopify 对象图——product、cart、customer、shop、request。用它在服务端填充负载，而不是依赖客户端调用。
* \*\*不要把 API 密钥放进公开仓库。\*\*它最终会出现在你的主题代码中并被发送到浏览器——这没有问题。但不要把同一份主题粘贴到公开仓库或在外部分享打包文件。
* \*\*使用 Catch all。\*\*当某个位置消失时，店面体验会显得残缺。配置少量安全默认商品的 Catch all 可以保持 UI 一致。
* \*\*在合适的地方缓存。\*\*Strategies API 在服务端有轻量缓存（`meta.servedFromCache`），但对于高流量的 PDP，你可能还希望在客户端对调用进行防抖或记忆化（例如，同一商品在一个会话中被渲染两次时不要重复调用）。

***

<div id="attribution">
  ## 归因
</div>

当顾客点击代码片段中的加购按钮时，`/cart/add.js` 调用会向购物车条目附加**行项目属性**：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "Aftersell",
  "_attribution": "CTA"
}
```

这些属性会随行项目一路传递到 Shopify 订单，出现在行项目记录中。你可以在下游用它们归因收入、筛选订单，或输入读取行项目属性的分析工具。

这里的键和值只是约定，不是必需的——无论你放什么，API 调用的行为都相同。你可以修改它们以适配自己的归因模型。例如：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "PDP Carousel",
  "_strategy_id": "{{ section.settings.strategy_id }}",
  "_campaign": "summer-2026"
}
```

<Tip>
  以下划线（`_`）开头的属性键会在购物车和结账 UI 中隐藏，但仍会附加到订单上。对于不希望顾客看到的仅用于归因的元数据，请使用下划线前缀。
</Tip>

在购物车上下文的实现中应用同样的模式——你从自定义追加销售区块发起的任何加购调用都可以携带你需要的任何属性。

<div id="attributing-back-to-the-evaluation">
  ### 归因回具体评估
</div>

要将订单归因回推荐该商品的**确切评估**——而不仅仅是"来自某个 Strategy"——请从响应中捕获 `evaluationId`，并在 `__as_offer_id` 属性下将其附加到行项目。AfterSell 会读取这个键，因此带有该标记的订单会在报告中归因到具体的评估。

在 `evaluate()` 处理函数中，保存响应中的 ID：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.then(function(data) {
  if (!data || !data.success || !data.products || !data.products.length) {
    clearSkeletons(); return;
  }
  evaluationId = data.evaluationId; // declare `var evaluationId;` alongside your other state
  renderCards(data.products.slice(0, MAX_PRODUCTS));
})
```

然后将其包含在加购属性中：

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: Object.assign(
  { "_source": "Aftersell", "_attribution": "CTA" },
  evaluationId ? { "__as_offer_id": evaluationId } : {}
)
```

请保留 `__as_offer_id` 中的双下划线——这是 AfterSell 查找的键，且下划线前缀可使其对顾客隐藏。如果 `evaluationId` 不存在（例如未返回任何商品），请跳过该属性，而不要发送空值。
