关于 b2c-hooks
面向 Salesforce B2C Commerce 开发者的 Hooks 技能,介绍 OCAPI/SCAPI、系统及自定义扩展点的注册与实现。内容涵盖返回值行为、请求内数据传递,以及订单支付授权流程。
主要功能
- 01Hook 注册文件与脚本结构
- 02HookMgr 调用与状态处理
- 03OCAPI/SCAPI Hook 行为
- 04request.custom 请求内传值
- 05订单支付授权与放置流程
适用场景
- 01扩展 OCAPI 或 SCAPI 行为
- 02实现订单计算等系统扩展
- 03设计订单支付授权与失败处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ salesforcecommercecloud/ b2c-developer-tooling --skill b2c-hooks安装后在 Claude Code 中输入/b2c-hooks确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/trigger-evals.jsonreferences/OCAPI-SCAPI-HOOKS.mdORDER-HOOK-LIFECYCLE.mdSYSTEM-HOOKS.md详细介绍
B2C Commerce 钩子
钩子是扩展点,可通过注册脚本自定义业务逻辑。B2C Commerce 支持两种钩子:
- OCAPI/SCAPI 钩子 - 通过 before、after 和 modifyResponse 钩子扩展 API 资源
- 系统钩子 - 用于订单计算、支付和其他核心功能的自定义扩展点
钩子类型概览
| 类型 | 用途 | 示例 |
|---|---|---|
| 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.ERROR | 400 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.calculate | calculate | 完整购物篮/订单计算 |
dw.order.calculateShipping | calculateShipping | 配送计算 |
dw.order.calculateTax | calculateTax | 税费计算 |
// 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.authorize | authorize | 支付授权 |
dw.order.payment.authorizeCreditCard | authorizeCreditCard | 由订单 PI POST/PATCH 调用的信用卡授权 |
dw.order.payment.capture | capture | 扣取已授权的款项 |
dw.order.payment.refund | refund | 退款 |
dw.order.payment.validateAuthorization | validateAuthorization | 检查授权有效性 |
dw.order.payment.reauthorize | reauthorize | 重新授权已过期的授权 |
订单钩子
| 扩展点 | 函数 | 用途 |
|---|---|---|
dw.order.createOrderNo | createOrderNo | 自定义订单号生成 |
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)。
详细参考
- OCAPI/SCAPI 钩子 - API 钩子模式和可用钩子
- 系统钩子 - 计算、支付和订单钩子
- 订单钩子生命周期 - 生命周期阶段、回滚语义和双钩子模式