跳到主要内容

用函数按商品加收附加费

背景​

有些商品在售价之外还要加收一笔费用,比如刻字商品的定制费、易碎商品的包装费,每个商品的金额都不一样。Shoplazza 的商品价格存在变体上,没有函数时,商家只能把这笔费用算进变体价格里,于是带来三个问题:

  • 商品页显示的是已经含费用的价格,买家看到的标价变高了,也分不清其中多少是附加费;
  • 费用一调整,商家就要逐个商品手动改变体价格,费用和价格很容易对不上;
  • 结账页只显示一个合计价格,买家看不出这里加了一笔附加费。

这三个问题的根源相同:买家付的就是变体价格,从加购到付款之间,没有任何环节能让应用在这个价格上按商品加一笔钱。因此本方案的核心目标是:商品标价保持不变,把附加费存在每个商品上,只在购物车和结账页把它加到商品行价格里,并在结账页的商品标题里标出这笔费用。

工作原理​

  1. 商家设置附加费:应用在安装时创建一个商品元字段定义,本例用的是 custom.markup。元字段是挂在商品等资源上的自定义字段。商家在需要加收费用的商品上给这个字段填整数金额,不收费的商品留空。
  2. 应用声明函数要读的字段:函数的 shoplazza.extension.toml 在 [[metafields_query]] 里列出 custom.markup,app deploy 时这份声明随函数一起提交。
  3. 安装时平台绑定函数:商家安装应用时,平台把已发布的函数连同声明一起绑定到店铺。
  4. 买家加购商品:平台以购物车为输入运行函数,每个商品行的 product.metafields 里带着该商品的 custom.markup 值。
  5. 函数返回调价结果:有附加费的商品行,函数返回等于附加费的 adjustment_fixed_price 和新标题;没有附加费的商品行不出现在输出里。
  6. 平台应用结果:商品行单价变为原价加 adjustment_fixed_price。创建结账时函数会再运行一次,且在所有折扣之前运行,新标题显示在结账页。

整体流程如下:

示例开发​

下面一步步实现这个函数。标价 100、custom.markup = 5 的商品,在购物车和结账页显示为 105,结账页标题显示为 Markup Tee (+5);没有这个元字段的商品价格不变。

前置条件​

  • 用 shoplazza app init 创建好的应用项目,见构建你的第一个应用。
  • Shoplazza CLI 2.2.5 或更高版本。更早的版本不会提交 [[metafields_query]] 声明,函数拿不到任何元字段。
  • shoplazza.app.toml 的 scopes 里包含 write_cart_transform,默认应用模板已经带上。
  • 一个用来安装应用的测试店铺。

函数的编译、发布、绑定等通用机制,见 Function 扩展开发指南。

第 1 步:应用安装时创建元字段定义​

元字段定义声明了某类资源上一个字段的命名空间、键和类型。商品元字段定义建好之后,店铺后台每个商品编辑页的底部都会出现这个字段,商家可以直接填写。

让应用在安装时通过接口创建定义,不要让商家手动去建。函数只读取它声明过的命名空间和键,商家手动建时只要填错一个,函数就找不到附加费,价格不变,也没有任何报错。由代码创建定义,可以保证定义、声明和函数代码里用的是同一个名字。

新增一个商品元字段来存附加费,比如本篇一直用的 custom.markup。名字可以自己取,但第 3 步的声明和第 4 步的函数代码里,要用同一组命名空间和键。

POST /openapi/2026-01/metafield_definition/product

请求体:

json5
{
"namespace": "custom",
"key": "markup",
"name": "Markup",
"type": "integer", // 附加费是整数金额
"description": "Amount added to the product price in the cart and at checkout"
}

响应(节选,新建的定义在 data.metafield_definition):

json
{
"code": "Success",
"data": {
"metafield_definition": {
"id": "705345636089727677",
"namespace": "custom",
"key": "markup",
"name": "Markup",
"owner_resource": "product",
"type": "integer"
}
}
}

定义属于店铺,每个店铺只需创建一次。完整字段见创建元字段定义。

之后由商家设置附加费:在店铺后台打开需要加收费用的商品的编辑页,在底部「元字段」里的 Markup 字段填入金额;不收费的商品留空。如果你的应用有自己的设置页,想让商家在那里填写,也可以由应用调用创建元字段接口写入。

第 2 步:创建函数扩展​

在应用项目根目录下,创建一个名为 markup-price 的函数扩展:

shell
shoplazza app extension create --type function --name markup-price

生成的目录如下:

extensions/
└── markup-price/
├── .gitignore
├── package.json
├── shoplazza.extension.toml # 扩展配置
└── src/
└── index.js # 函数入口文件

第 3 步:声明函数要读取的元字段​

函数只能拿到它声明过的元字段。在 extensions/markup-price/shoplazza.extension.toml 里加一个 [[metafields_query]] 块:

toml
id = ""
name = "markup-price"
type = "function"

# 把商品元字段 custom.markup 加载到每个商品行的 product.metafields 里
[[metafields_query]]
resource = "product"
namespace = "custom"
key = "markup"
  • resource:product 或 shop。商品元字段出现在每个商品行的 product.metafields 里;
  • namespace 和 key:要和第 1 步的定义一致;
  • 一个函数最多声明 5 条。app deploy 会校验声明,不合法时发布失败。
备注

不写这个块,函数照常运行,但 product.metafields 是空数组 []。函数找不到附加费,所有价格都不变,也不会有任何报错。价格没变时,先检查这个块。

第 4 步:编写函数逻辑​

函数从 stdin 读入购物车,把结果写到 stdout。脚手架生成的 src/index.js 里已经有负责读写的 readInput() 和 writeOutput(),保留它们,把 run() 换成下面的逻辑:

javascript
// extensions/markup-price/src/index.js

const input = readInput();
const result = run(input);
writeOutput(result);

function run(input) {
// 收集需要调价的商品行,每个有附加费的商品行一条
const update = [];

input.cart.line_items.forEach((lineItem) => {
// [[metafields_query]] 里声明过的元字段;没有声明时是空数组
const metafields = lineItem.product.metafields || [];

// 找到这个商品的附加费:命名空间为 custom、键为 markup 的元字段
const markupField = metafields.find(
(metafield) => metafield.namespace === "custom" && metafield.key === "markup"
);

// 这个商品没有附加费:不输出该商品行,它保持原价
if (!markupField) {
return;
}

// parseFloat 数字和数字字符串都能处理
const markup = parseFloat(markupField.value);

// adjustment_fixed_price 必须在 0 到 999999999 之间。
// 只要有一行不合法,平台会拒绝整个输出,所以在这里跳过不合法的值。
if (!Number.isFinite(markup) || markup < 0 || markup > 999999999) {
return;
}

update.push({
// 要修改的商品行:用输入里的商品行 id
id: lineItem.id,
price: {
// 加在原单价上的金额,保留 2 位小数的字符串。
// 最终单价 = 原价 + adjustment_fixed_price。
adjustment_fixed_price: markup.toFixed(2),
},
// 新的商品行标题,显示在结账页;购物车页仍显示原标题
title: `${lineItem.product.product_title} (+${markup})`,
});
});

// update 是唯一支持的操作
return { operation: { update } };
}

// readInput() 和 writeOutput() 保持脚手架原样

购物车里有两个商品,一个 custom.markup = 5、一个没有这个元字段时,函数收到的输入(节选):

jsonc
{
"cart": {
"line_items": [
{
"id": "line-a",
"quantity": 1,
"product": {
"price": "100.00",
"product_title": "Markup Tee",
"metafields": [{ "namespace": "custom", "key": "markup", "value": 5 }]
}
},
{
"id": "line-b",
"quantity": 1,
"product": {
"price": "50.00",
"product_title": "Plain Cap",
"metafields": []
}
}
]
}
}

返回:

json
{
"operation": {
"update": [
{
"id": "line-a",
"price": { "adjustment_fixed_price": "5.00" },
"title": "Markup Tee (+5)"
}
]
}
}

line-b 不在输出里,所以保持 50。完整的输入输出约定见编写函数逻辑。

注意

adjustment_fixed_price 是加在原价上的金额,不是替换原价。标价 100 的商品返回 "105.00",商品行会变成 205。只返回附加费本身。

第 5 步:发布应用​

在应用项目根目录下发布应用。这一步会编译函数,把它和声明一起上传,并生成一个新的应用版本:

shell
shoplazza app deploy

输出里函数那一项带有 input_query,也就是第 3 步的声明:

json
{
"extension_id": "4330659779216150544",
"name": "markup-price",
"type": "function",
"version": "1.0.3",
"input_query": "{\"metafields_query\":[{\"resource\":\"product\",\"namespace\":\"custom\",\"key\":\"markup\"}]}"
}

函数那一项没有 input_query,说明声明没有提交上去,检查 CLI 版本和 [[metafields_query]] 块。

第 6 步:在测试店铺安装应用​

平台在应用安装时把函数绑定到店铺。在 Partner 后台打开你的应用,在「测试App」里选择测试店铺,点「安装App」。

注意

在这次发布之前就装了应用的店铺,会继续运行它绑定时的函数版本,重新发布不会让它切换过去。要测试新的函数版本或修改后的声明,在测试店铺上卸载并重新安装应用。

验证​

在测试店铺上:

  1. 把设置了 custom.markup = 5、标价 100 的商品加入购物车。商品行显示 105,标题不变;
  2. 再加购一个没有这个元字段的商品,价格保持标价;
  3. 进入结账页,第一个商品行仍是 105,标题显示为 Markup Tee (+5);
  4. 打开商品页,仍显示标价 100。
备注

给购物车里已有的商品行增加数量,价格仍是加购时算出的价格。改了附加费或函数之后,先删掉这一行再重新加购,才能看到新价格。

结账页效果如下:

结账页商品行标题为 Markup Tee (+5)、价格为 105

下一步​