mcpField
B
b2c-hooks

b2c-hooks

github.com/salesforcecommercecloud开发5790 次安装今天更新
代码仓库

关于 b2c-hooks

面向 Salesforce B2C Commerce 开发者的 Hooks 技能,介绍 OCAPI/SCAPI、系统及自定义扩展点的注册与实现。内容涵盖返回值行为、请求内数据传递,以及订单支付授权流程。

主要功能

  1. 01Hook 注册文件与脚本结构
  2. 02HookMgr 调用与状态处理
  3. 03OCAPI/SCAPI Hook 行为
  4. 04request.custom 请求内传值
  5. 05订单支付授权与放置流程

适用场景

  1. 01扩展 OCAPI 或 SCAPI 行为
  2. 02实现订单计算等系统扩展
  3. 03设计订单支付授权与失败处理

安装

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

$npx skills add https://github.com/salesforcecommercecloud/b2c-developer-tooling --skill b2c-hooks

安装后在 Claude Code 中输入/b2c-hooks确认已加载。

包含的文件

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

evals/
trigger-evals.json
references/
OCAPI-SCAPI-HOOKS.md
ORDER-HOOK-LIFECYCLE.md
SYSTEM-HOOKS.md

详细介绍

B2C Commerce 钩子

钩子是扩展点,可通过注册脚本自定义业务逻辑。B2C Commerce 支持两种钩子:

  1. OCAPI/SCAPI 钩子 - 通过 before、after 和 modifyResponse 钩子扩展 API 资源
  2. 系统钩子 - 用于订单计算、支付和其他核心功能的自定义扩展点

钩子类型概览

类型用途示例
OCAPI/SCAPI扩展 API 行为dw.ocapi.shop.basket.afterPOST
系统核心业务逻辑dw.order.calculate
自定义自有扩展点app.checkout.validate

注册钩子

文件结构
my_cartridge/
├── package.json           # References hooks.json
└── cartridge/
    └── scripts/
        ├── hooks.json     # Hook registrations
        └── hooks/         # Hook implementations
            ├── basket.js
            └── order.js
package.json

引用钩子配置文件:

{
  "name": "my_cartridge",
  "hooks": "./cartridge/scripts/hooks.json"
}
hooks.json

注册钩子及其实现脚本:

{
  "hooks": [
    {
      "name": "dw.ocapi.shop.basket.afterPOST",
      "script": "./hooks/basket.js"
    },
    {
      "name": "dw.ocapi.shop.basket.modifyPOSTResponse",
      "script": "./hooks/basket.js"
    },
    {
      "name": "dw.order.calculate",
      "script": "./hooks/order.js"
    }
  ]
}
钩子脚本

导出与钩子方法名称(不含包前缀)匹配的函数:

// hooks/basket.js
var Status = require('dw/system/Status');

exports.afterPOST = function(basket) {
    // Called after basket creation
    // Returning a value would skip system implementation
};

exports.modifyPOSTResponse = function(basket, basketResponse) {
    // Modify the API response
    basketResponse.c_customField = 'value';
};

HookMgr API

使用 dw.system.HookMgr 以编程方式调用钩子:

var HookMgr = require('dw/system/HookMgr');

// Check if hook exists
if (HookMgr.hasHook('dw.order.calculate')) {
    // Call the hook
    var result = HookMgr.callHook('dw.order.calculate', 'calculate', basket);
}
方法说明
hasHook(extensionPoint)如果钩子已注册或具有默认实现,则返回 true
callHook(extensionPoint, functionName, args...)调用钩子,返回结果或 undefined

Status 对象

钩子返回 dw.system.Status,用于指示成功或失败:

var Status = require('dw/system/Status');

// Success - continue processing
return new Status(Status.OK);

// Error - stop processing, rollback transaction
var status = new Status(Status.ERROR);
status.addDetail('error_code', 'INVALID_ADDRESS');
status.addDetail('message', 'Address validation failed');
return status;
状态HTTP 响应行为
Status.OK继续钩子执行继续
Status.ERROR400 Bad Request回滚事务,停止处理
未捕获的异常500 Internal Error回滚事务

返回值行为(重要)

返回任何值的 OCAPI/SCAPI 钩子都会跳过系统实现,以及该扩展点上后续注册的所有钩子。

这是常见的错误来源。例如,如果钩子返回 Status.OK,系统的 dw.order.calculate 实现将不会运行,导致购物车总额不正确。

何时返回值

仅在以下情况下返回 Status 对象:

  • 因错误而停止处理(Status.ERROR)
  • 有意跳过系统实现
何时不返回值

若要确保系统实现运行(例如购物车计算),不要返回任何值:

// Returning Status.OK skips system implementation
exports.afterPOST = function(basket) {
    doSomething(basket);
    return new Status(Status.OK);  // Skips dw.order.calculate
};

// No return value - system implementation runs
exports.afterPOST = function(basket) {
    doSomething(basket);
    // No return, or explicit: return;
};
总结
返回值OCAPI/SCAPI 行为自定义钩子行为
undefined(不返回)系统实现运行,后续钩子也运行所有钩子均运行
Status.OK跳过系统实现和后续钩子所有钩子均运行
Status.ERROR停止处理并返回错误所有钩子均运行

调试提示:如果购物车总额不正确或钩子未触发,请检查较早执行的钩子是否返回了值。

OCAPI/SCAPI 钩子

OCAPI 和 SCAPI 共用相同的钩子。在 Business Manager 中启用: 管理 > 全局首选项 > 功能开关 > 启用 Salesforce Commerce Cloud API 钩子执行

钩子类型
钩子调用时机用途
before<METHOD>处理前验证、访问控制
after<METHOD>处理后(在事务中)修改数据、外部调用
modify<METHOD>Response发送响应前添加或修改响应属性
常见钩子模式
// Validation in beforePUT
exports.beforePUT = function(basket, addressDoc) {
    if (!isValidAddress(addressDoc)) {
        var status = new Status(Status.ERROR);
        status.addDetail('validation_error', 'Invalid address');
        return status;
    }
};

// External call in afterPOST (within transaction)
exports.afterPOST = function(basket, paymentDoc) {
    var result = callPaymentService(paymentDoc);
    request.custom.paymentResult = result; // Pass to modifyResponse
    // Returning a Status would skip system implementation
};

// Modify response
exports.modifyPOSTResponse = function(basket, basketResponse, paymentDoc) {
    basketResponse.c_paymentStatus = request.custom.paymentResult.status;
};
在钩子之间传递数据

使用 request.custom 在同一请求中的钩子之间传递数据:

// In afterPOST
exports.afterPOST = function(basket, doc) {
    request.custom.externalId = callExternalService();
};

// In modifyPOSTResponse
exports.modifyPOSTResponse = function(basket, response, doc) {
    response.c_externalId = request.custom.externalId;
};
区分 SCAPI 与 OCAPI
exports.afterPOST = function(basket) {
    if (request.isSCAPI()) {
        // SCAPI-specific logic
    } else {
        // OCAPI-specific logic
    }
};

无头订单支付:使用 Order-PI 授权接缝

优先采用有文档说明的 Shopper Orders 生命周期。默认情况下,不要在 dw.ocapi.shop.order.afterPOST 中授权支付:

POST /orders
  -> order persists in CREATED
PATCH /orders/{orderNo}/payment-instruments/{paymentInstrumentId}
  -> platform updates the order PI
  -> dw.order.payment.authorizeCreditCard or dw.order.payment.authorize
  -> order.payment_instrument.afterPATCH(..., successfullyAuthorized)
  -> default afterPATCH places the order when authorized coverage >= order total

这样可以将订单创建与网关调用分开,保留可用于恢复的持久订单号,使用与标准 Commerce 实现和支付扩展相同的授权扩展点,并由平台负责最终基于覆盖情况的下单处理。

授权语义

订单 PI 的 PATCH 按以下方式选择支付钩子:

  • 要求所选支付方式处于启用状态且适用于该订单。对于 CREDIT_CARD,请在请求中包含 paymentCard。
  • 如果请求包含 paymentCard,或订单 PI 包含信用卡类型,则调用 dw.order.payment.authorizeCreditCard。
  • 否则调用 dw.order.payment.authorize。
  • 对于已保存的支付方式,传入文档中指定的客户支付工具 ID(在 OCAPI 文档中称为 customer_payment_instrument_id;生成的 SCAPI 客户端可能会将其公开为驼峰命名属性)。Commerce 会从订单客户的支付工具中解析该 ID,并复制对应的 PI 以进行授权;未知 ID 会导致抛出异常。请求中的金额和卡安全码仍会传递给 authorizeCreditCard。
  • 如果没有客户 PI 引用,则会根据请求数据(例如 paymentCard 或银行账户信息)填充授权输入。不要假设 paymentDetails 参数与已附加到 order 的持久化 PI 是同一个对象实例;如果提供商状态存储在持久化工具中,请解析并验证该工具。
  • 不要接受客户端提供的可重复使用支付令牌作为所有权替代凭据。
  • 在授权实现中设置支付交易的处理器、金额、类型和提供商交易标识符。仅在授权成功时返回 Status.OK,否则返回 Status.ERROR。

注册 dw.order.payment.authorizeCreditCard、dw.order.payment.authorize 以及订单 PI 的 beforePATCH/afterPATCH 钩子。在 beforePATCH 中,在调用任何网关之前,根据持久化订单状态验证不可信请求:PI 数量、支付方式、适用的处理器、由服务端管理的支付分配金额、金额、所有权/令牌来源以及集成状态。对于单 PI 结账,可以要求请求金额等于订单总额;对于拆分支付,应验证每笔由服务端管理的分配金额。

使用文档中规定的全部四个 afterPATCH 参数。平台会将支付授权的 Status 简化为 successfullyAuthorized;只有在同一请求中确实需要区分时,才在 request.custom 中保留更丰富的处理器结果:

var Status = require('dw/system/Status');

exports.afterPATCH = function (order, paymentInstrument, newPaymentInstrument, successfullyAuthorized) {
    if (!successfullyAuthorized) {
        if (request.custom.deterministicPaymentDecline === true) {
            return new Status(Status.ERROR, 'PAYMENT_DECLINED');
        }
        return; // Preserve indeterminate/review state for reconciliation.
    }

    var validation = validateAuthorizedInstrument(order, paymentInstrument);
    if (validation) { return validation; }
    // Return undefined so the default placement implementation still runs.
};

successfullyAuthorized === true 仅适用于刚处理的支付工具;这本身并不能证明多支付工具订单已获得全额支付。应让默认实现仅在已授权支付金额等于或超过订单总额时,才将仍处于 CREATED 状态的订单下单。处理后,订单状态会变为 NEW,并为导出做好准备。

商店前端/BFF 结果处理

将返回的订单状态视为权威的下单结果:

  • NEW:下单成功。
  • CREATED:订单未下单。授权多个支付工具时,这可能是预期状态;也可能表示拒付、审核或状态未知。
  • 确定的授权失败:调用 Shopper Orders POST /orders/{orderNo}/actions/fail?reopenBasket=true,并使用原因代码 payment_auth_failure,以便 Commerce 持久化失败订单并恢复购物篮。
  • 网关结果不确定:不要自动重试或重新打开购物篮。保留 CREATED 订单以供对账,避免重复授权。

在此推荐流程中,应使用 Shopper Orders 的失败操作执行持久化状态转换,而不是在订单 POST 钩子之间协调失败处理。

替代方案:在 order.afterPOST 中授权并下单

当结账流程需要一个服务端阶段,在订单创建事务中创建订单、为每个 PI 授权,并执行 Commerce 侧的下单或失败转换;或者处理器无法参与订单 PI 授权端点时,请使用 dw.ocapi.shop.order.afterPOST。这样可以简化店面编排。外部网关的副作用不会随 Commerce 事务一起回滚,因此请权衡这一优势与以下取舍:

  • 外部支付调用会在订单创建的平台事务中运行,并占用订单 POST 的超时额度。
  • 返回 Status.ERROR 会回滚订单创建,因此调用方可能同时丢失订单和原始购物篮引用。
  • 实现必须手动授权每个 PI、验证总额是否足够、调用 OrderMgr.placeOrder/failOrder,并保留平台或 cartridge 后续行为。
  • 请勿在 afterPOST 中为 OrderMgr.placeOrder() 或 OrderMgr.failOrder() 添加 Transaction.wrap();该钩子已在平台事务中运行。

如果选择此方案,请参阅 订单钩子生命周期,了解事务语义以及在返回 HTTP 错误的同时保留失败订单的双钩子变体。

使用 request.custom 传递钩子间数据

仅将 request.custom 用作请求范围内的钩子阶段间传输机制。不要仅为了在同一请求中将更丰富的结果从 authorize* 传递到 afterPATCH 而添加持久化元数据。

系统钩子

计算钩子
扩展点函数用途
dw.order.calculatecalculate完整购物篮/订单计算
dw.order.calculateShippingcalculateShipping配送计算
dw.order.calculateTaxcalculateTax税费计算
// hooks/calculate.js
var Status = require('dw/system/Status');
var HookMgr = require('dw/system/HookMgr');

exports.calculate = function(lineItemCtnr) {
    // Calculate shipping
    HookMgr.callHook('dw.order.calculateShipping', 'calculateShipping', lineItemCtnr);

    // Calculate promotions, totals...

    // Calculate tax
    HookMgr.callHook('dw.order.calculateTax', 'calculateTax', lineItemCtnr);

    return new Status(Status.OK);
};
支付钩子
扩展点函数用途
dw.order.payment.authorizeauthorize支付授权
dw.order.payment.authorizeCreditCardauthorizeCreditCard由订单 PI POST/PATCH 调用的信用卡授权
dw.order.payment.capturecapture扣取已授权的款项
dw.order.payment.refundrefund退款
dw.order.payment.validateAuthorizationvalidateAuthorization检查授权有效性
dw.order.payment.reauthorizereauthorize重新授权已过期的授权
订单钩子
扩展点函数用途
dw.order.createOrderNocreateOrderNo自定义订单号生成
var OrderMgr = require('dw/order/OrderMgr');
var Site = require('dw/system/Site');

exports.createOrderNo = function() {
    var seqNo = OrderMgr.createOrderSequenceNo();
    var prefix = Site.current.ID;
    return prefix + '-' + seqNo;
};

自定义钩子

创建自己的扩展点:

// Define custom hook
var HookMgr = require('dw/system/HookMgr');

function processCheckout(basket) {
    // Call custom hook if registered
    if (HookMgr.hasHook('app.checkout.validate')) {
        var status = HookMgr.callHook('app.checkout.validate', 'validate', basket);
        if (status && status.error) {
            return status;
        }
    }
    // Continue processing...
}

在 hooks.json 中注册:

{
  "hooks": [
    {
      "name": "app.checkout.validate",
      "script": "./hooks/checkout.js"
    }
  ]
}

无论返回值如何,自定义钩子都会执行所有已注册的实现。

钩子中的远程包含

使用其他 SCAPI 端点的数据增强 API 响应:

var RESTResponseMgr = require('dw/system/RESTResponseMgr');

exports.modifyGETResponse = function(product, doc) {
    // Include Custom API response
    var include = RESTResponseMgr.createScapiRemoteInclude(
        'custom',           // API family
        'my-api',           // API name
        'v1',               // Version
        'endpoint'          // Endpoint
    );
    doc.c_additionalData = { value: [include] };
};

最佳实践

  • 从 OCAPI/SCAPI 钩子返回 undefined(不返回值),以确保系统实现运行
  • 仅在需要停止处理时返回 Status.ERROR
  • 返回 Status.OK 会跳过系统实现和后续钩子
  • 使用 request.custom 在钩子之间传递数据
  • 同时支持两种 API 时,检查 request.isSCAPI()
  • 让钩子保持专注并具备高性能
  • 在 modifyResponse 中使用自定义属性(c_ 前缀)
  • 避免在计算钩子中使用事务(会破坏 SCAPI)
  • 避免在 beforeGET 中进行缓慢的外部调用(会影响缓存)

错误处理

熔断器

钩子错误过多会触发熔断器(HTTP 503):

{
  "title": "Hook Circuit Breaker",
  "type": "https://api.commercecloud.salesforce.com/.../hook-circuit-breaker",
  "detail": "Failure rate above threshold of '50%'",
  "extensionPointName": "dw.ocapi.shop.basket.afterPOST"
}
超时

钩子必须在 SCAPI 超时时限内完成(超时会返回 HTTP 504)。

详细参考

评论

支持 Markdown