mcpField
S
sfcc-ocapi-scapi

sfcc-ocapi-scapi

github.com/finsilabs开发6080 次安装6 天前更新
代码仓库

关于 sfcc-ocapi-scapi

面向 Salesforce B2C Commerce 开发者的技能,说明如何使用 SCAPI、Shopper APIs 和 OCAPI 构建店面及管理操作,并涵盖 SLAS 身份验证。

主要功能

  1. 01区分 SCAPI 与 OCAPI 的用途
  2. 02实施 SLAS 令牌认证
  3. 03查询商品目录并管理购物篮
  4. 04通过 OCAPI 执行服务端管理操作
  5. 05涵盖迁移与常见故障排查

适用场景

  1. 01构建无头 B2C 店面
  2. 02为移动应用接入商品、购物车和结账
  3. 03使用 OCAPI 集成订单管理
  4. 04将现有 OCAPI 集成迁移至 SCAPI

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill sfcc-ocapi-scapi

安装后在 Claude Code 中输入/sfcc-ocapi-scapi确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

tile.json
evals/ocapi-data-api-admin-operations-and-orde/
criteria.json
task.md
evals/scapi-headless-catalog-integration-with-/
criteria.json
task.md
evals/slas-authentication-and-session-manageme/
criteria.json
task.md

详细介绍

SFCC OCAPI 和 Shopper API

概述

Salesforce B2C Commerce 提供两类 API:使用 Basic Auth 或 OAuth 的旧版 Open Commerce API(OCAPI),以及使用 SLAS(Shopper Login and API Access Service)令牌的现代 Commerce API(SCAPI/Shopper API)。对于无头店面、PWA Kit 和第三方集成,推荐使用 SCAPI。OCAPI 仍是服务器端管理操作(产品导入、订单管理、促销管理)的主要 Data API。Composable Storefront(原 PWA Kit)完全基于 SCAPI 构建。

何时使用此技能

  • 使用 SFCC 作为商务后端构建无头 B2C 店面时
  • 使用自定义 API 调用实现 Salesforce PWA Kit(Composable Storefront)时
  • 将移动应用与 SFCC 产品目录、购物车和结账流程集成时
  • 使用 OCAPI Data API 构建服务器端订单管理集成时
  • 将现有 OCAPI 集成迁移到较新的 SCAPI 端点时
  • 为客户身份验证流程实现 SLAS 令牌管理时

核心说明

  1. 了解 API 体系

    API身份验证使用场景
    SCAPI Shopper APIsSLAS 访客/客户令牌无头店面、产品搜索、购物车、结账
    OCAPI Shop APIBasic/OAuth可信服务器环境中的店面操作
    OCAPI Data API客户端凭据管理操作:产品导入、订单管理、促销
    SCAPI Admin APIs客户端凭据现代管理操作(逐步取代 OCAPI Data API)

    基础 URL 模式:https://{shortCode}.api.commercecloud.salesforce.com/

  2. 使用 SLAS(Shopper Login and API Access Service)进行身份验证

    // lib/sfcc-auth.ts
    const SLAS_BASE = `https://${process.env.SFCC_SHORT_CODE}.api.commercecloud.salesforce.com/shopper/auth/v1`;
    const ORG_ID = process.env.SFCC_ORG_ID!; // f_ecom_xxx format
    const CLIENT_ID = process.env.SFCC_SLAS_CLIENT_ID!;
    
    // Step 1: Get PKCE code challenge for guest token
    function generateCodeChallenge(): { verifier: string; challenge: string } {
      const nodeCrypto = require("crypto");
      const verifier = nodeCrypto.randomBytes(32).toString("hex");
      const hash = nodeCrypto.createHash("sha256").update(verifier).digest("base64url");
      return { verifier, challenge: hash };
    }
    
    // Get a guest access token
    export async function getGuestToken(): Promise<{ access_token: string; refresh_token: string }> {
      const { verifier, challenge } = generateCodeChallenge();
    
      // Step 1: Authorize (get auth code)
      const authorizeUrl = new URL(`${SLAS_BASE}/organizations/${ORG_ID}/oauth2/authorize`);
      authorizeUrl.searchParams.set("client_id", CLIENT_ID);
      authorizeUrl.searchParams.set("channel_id", process.env.SFCC_SITE_ID!);
      authorizeUrl.searchParams.set("redirect_uri", process.env.SFCC_SLAS_REDIRECT_URI!);
      authorizeUrl.searchParams.set("response_type", "code");
      authorizeUrl.searchParams.set("code_challenge", challenge);
      authorizeUrl.searchParams.set("hint", "guest");
    
      // SFCC returns a redirect — extract the code from the Location header
      const authResponse = await fetch(authorizeUrl.toString(), { redirect: "manual" });
      const location = authResponse.headers.get("location") ?? "";
      const code = new URL(location).searchParams.get("code") ?? "";
    
      // Step 2: Exchange code for token
      const tokenResponse = await fetch(
        `${SLAS_BASE}/organizations/${ORG_ID}/oauth2/token`,
        {
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded" },
          body: new URLSearchParams({
            grant_type: "authorization_code_pkce",
            code,
            code_verifier: verifier,
            client_id: CLIENT_ID,
            redirect_uri: process.env.SFCC_SLAS_REDIRECT_URI!,
            channel_id: process.env.SFCC_SITE_ID!,
          }),
        }
      );
    
      return tokenResponse.json();
    }
    
    // Refresh an access token
    export async function refreshToken(refreshToken: string) {
      const response = await fetch(`${SLAS_BASE}/organizations/${ORG_ID}/oauth2/token`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: refreshToken,
          client_id: CLIENT_ID,
        }),
      });
      return response.json();
    }
    
  3. 使用 Shopper Products API 查询产品目录

    // lib/sfcc-products.ts
    const API_BASE = `https://${process.env.SFCC_SHORT_CODE}.api.commercecloud.salesforce.com`;
    const ORG_ID = process.env.SFCC_ORG_ID!;
    const SITE_ID = process.env.SFCC_SITE_ID!;
    
    // Search products
    export async function searchProducts(params: {
      q?: string;
      categoryId?: string;
      start?: number;
      count?: number;
      sortKey?: string;
    }, accessToken: string) {
      const url = new URL(`${API_BASE}/search/shopper-search/v1/organizations/${ORG_ID}/product-search`);
      url.searchParams.set("siteId", SITE_ID);
      if (params.q) url.searchParams.set("q", params.q);
      if (params.categoryId) url.searchParams.set("refine", `cgid=${params.categoryId}`);
      url.searchParams.set("start", (params.start ?? 0).toString());
      url.searchParams.set("count", (params.count ?? 20).toString());
      if (params.sortKey) url.searchParams.set("sort", params.sortKey);
      url.searchParams.set("expand", "images,prices,availability,variations");
    
      const response = await fetch(url.toString(), {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      return response.json();
    }
    
    // Get product by ID
    export async function getProduct(productId: string, accessToken: string) {
      const url = `${API_BASE}/product/shopper-products/v1/organizations/${ORG_ID}/products/${productId}?siteId=${SITE_ID}&expand=images,prices,availability,variations,promotions`;
      const response = await fetch(url, {
        headers: { Authorization: `Bearer ${accessToken}` },
      });
      return response.json();
    }
    
  4. 使用 Shopper Baskets API 管理购物车和结账流程

    // lib/sfcc-basket.ts
    
    // Create a basket
    export async function createBasket(accessToken: string) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets?siteId=${SITE_ID}`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ customerInfo: { email: null } }),
        }
      );
      return response.json(); // { basketId: "xxx", ... }
    }
    
    // Add item to basket
    export async function addItemToBasket(
      basketId: string,
      productId: string,
      quantity: number,
      accessToken: string
    ) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets/${basketId}/items?siteId=${SITE_ID}`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify([{ productId, quantity }]),
        }
      );
      return response.json();
    }
    
    // Set shipping address
    export async function setShipmentAddress(
      basketId: string,
      shipmentId = "me",
      address: Record<string, string>,
      accessToken: string
    ) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets/${basketId}/shipments/${shipmentId}/shipping-address?siteId=${SITE_ID}`,
        {
          method: "PUT",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(address),
        }
      );
      return response.json();
    }
    
    // Submit order
    export async function submitOrder(basketId: string, accessToken: string) {
      const response = await fetch(
        `${API_BASE}/checkout/shopper-orders/v1/organizations/${ORG_ID}/orders?siteId=${SITE_ID}`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ basketId }),
        }
      );
      return response.json(); // { orderNo: "00001234", status: "created" }
    }
    
  5. 使用 OCAPI Data API 执行服务器端管理操作

    OCAPI Data API 使用客户端凭据 OAuth2 令牌:

    // lib/sfcc-ocapi.ts
    
    // Get admin token via client credentials
    async function getAdminToken(): Promise<string> {
      const response = await fetch("https://account.demandware.com/dwsso/oauth2/access_token", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          Authorization: `Basic ${Buffer.from(
            `${process.env.SFCC_OCAPI_CLIENT_ID}:${process.env.SFCC_OCAPI_CLIENT_SECRET}`
          ).toString("base64")}`,
        },
        body: new URLSearchParams({ grant_type: "client_credentials" }),
      });
      const { access_token } = await response.json();
      return access_token;
    }
    
    // Get order via OCAPI Data API
    export async function getOrder(orderNo: string): Promise<Record<string, unknown>> {
      const token = await getAdminToken();
      const instanceUrl = process.env.SFCC_INSTANCE_URL!; // https://xxxx.dx.commercecloud.salesforce.com
    
      const response = await fetch(
        `${instanceUrl}/s/${process.env.SFCC_SITE_ID}/dw/data/v23_2/orders/${orderNo}`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
          },
        }
      );
      return response.json();
    }
    
    // Update order status
    export async function updateOrderStatus(orderNo: string, status: string): Promise<void> {
      const token = await getAdminToken();
      const instanceUrl = process.env.SFCC_INSTANCE_URL!;
    
      await fetch(
        `${instanceUrl}/s/${process.env.SFCC_SITE_ID}/dw/data/v23_2/orders/${orderNo}`,
        {
          method: "PATCH",
          headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ status }),
        }
      );
    }
    

示例

客户登录与购物车合并(SCAPI)
export async function loginCustomer(
  email: string,
  password: string,
  guestToken: string
): Promise<{ access_token: string; customer_id: string }> {
  // Step 1: Get login token via Trusted Agent SLAS flow
  const response = await fetch(
    `${SLAS_BASE}/organizations/${ORG_ID}/oauth2/login`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Authorization: `Bearer ${guestToken}`,
      },
      body: new URLSearchParams({
        type: "credentials",
        username: email,
        password,
        client_id: CLIENT_ID,
      }),
    }
  );

  const { access_token, customer_id } = await response.json();

  // Step 2: Merge guest basket into customer basket
  if (guestBasketId) {
    await fetch(
      `${API_BASE}/checkout/shopper-baskets/v1/organizations/${ORG_ID}/baskets?siteId=${SITE_ID}`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${access_token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ guestBasketId }), // Triggers merge
      }
    );
  }

  return { access_token, customer_id };
}
通过 Data API 导入 OCAPI 产品
// Import/update products via OCAPI Data API
export async function upsertProduct(product: {
  id: string;
  name: string;
  longDescription?: string;
  price: number;
}) {
  const token = await getAdminToken();
  const instanceUrl = process.env.SFCC_INSTANCE_URL!;

  const ocapiProduct = {
    id: product.id,
    name: { default: product.name },
    long_description: { default: product.longDescription ?? "" },
    price: product.price,
    classification_category: { id: "electronics" },
  };

  const response = await fetch(
    `${instanceUrl}/s/-/dw/data/v23_2/products/${product.id}`,
    {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(ocapiProduct),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`OCAPI product upsert failed: ${error.message}`);
  }

  return response.json();
}

最佳实践

  • 所有新的无头项目都使用 SCAPI/Shopper API — OCAPI 处于维护模式;SCAPI 具有更好的速率限制、基于 JWT 的身份验证,并且是 Salesforce 当前重点投入的方向
  • 主动实现令牌刷新 — SLAS 访问令牌会在 30 分钟后过期;应在令牌过期前于后台刷新,而不是等到出现 401 错误
  • 将令牌存储在 httpOnly Cookie 中 — 客户端 JavaScript 不应直接访问 SLAS 令牌;应在服务器端设置 Cookie,并使用服务器端代理调用 API
  • 使用 Salesforce Commerce SDK(React 使用 @salesforce/commerce-sdk-react,Node.js 使用 commerce-sdk-isomorphic),以获取类型安全的 API 客户端和自动令牌管理功能
  • 绝不公开 OCAPI 客户端凭据 — Data API 凭据具有管理员级别的访问权限;请使用环境变量,并仅在服务器端代码路径中使用
  • 使用 select 参数限制字段 — SFCC API 响应内容较多;对于列表端点,?select=(id,name,price_min) 可大幅减小响应体积
  • 为订单提交实现请求幂等性 — SFCC 购物车可能出现竞态条件;提交订单时请使用 Idempotency-Key 标头

常见问题

问题解决方案
SLAS 授权返回 400在 Business Manager 的 SLAS 配置中确认已注册 redirect_uri;该 URI 必须完全匹配
访客令牌无法查看已发布的产品在 Business Manager → Site Preferences → OCAPI Settings 中检查访客客户端 ID 的 OCAPI/Shop API 权限
购物篮合并静默失败访客购物篮必须由与客户会话相同的 SLAS 客户端 ID 创建;不支持跨客户端合并购物篮
即使令牌有效,OCAPI 仍返回 401OCAPI Data API 要求客户端 ID 注册了 data 资源权限,而不仅仅是 shop 资源权限
产品搜索返回 0 条结果在 Business Manager → Site Preferences → Search 中检查搜索索引状态;另请确认 siteId 参数与正确的站点匹配
超出 SCAPI 速率限制(429)实施指数退避;考虑在服务端缓存目录数据(产品列表很少实时变化)

相关技能

  • @sfcc-cartridge-development
  • @sfcc-business-manager
  • @headless-commerce-architecture
  • @oauth-implementation
  • @shopify-storefront-api

评论

支持 Markdown