用函数按商品加收附加费
背景
有些商品在售价之外还要加收一笔费用,比如刻字商品的定制费、易碎商品的包装费,每个商品的金额都不一样。Shoplazza 的商品价格存在变体上,没有函数时,商家只能把这笔费用算进变体价格里,于是带来三个问题:
- 商品页显示的是已经含费用的价格,买家看到的标价变高了,也分不清其中多少是附加费;
- 费用一调整,商家就要逐个商品手动改变体价格,费用和价格很容易对不上;
- 结账页只显示一个合计价格,买家看不出这里加了一笔附加费。
这三个问题的根源相同:买家付的就是变体价格,从加购到付款之间,没有任何环节能让应用在这个价格上按商品加一笔钱。因此本方案的核心目标是:商品标价保持不变,把附加费存在每个商品上,只在购物车和结账页把它加到商品行价格里,并在结账页的商品标题里标出这笔费用。
工作原理
- 商家设置附加费:应用在安装时创建一个商品元字段定义,本例用的是
custom.markup。元字段是挂在商品等资源上的自定义字段。商家在需要加收费用的商品上给这个字段填整数金额,不收费的商品留空。 - 应用声明函数要读的字段:函数的
shoplazza.extension.toml在[[metafields_query]]里列出custom.markup,app deploy时这份声明随函数一起提交。 - 安装时平台绑定函数:商家安装应用时,平台把已发布的函数连同声明一起绑定到店铺。
- 买家加购商品:平台以购物车为输入运行函数,每个商品行的
product.metafields里带着该商品的custom.markup值。 - 函数返回调价结果:有附加费的商品行,函数返回等于附加费的
adjustment_fixed_price和新标题;没有附加费的商品行不出现在输出里。 - 平台应用结果:商品行单价变为原价加
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
请求体:
{
"namespace": "custom",
"key": "markup",
"name": "Markup",
"type": "integer", // 附加费是整数金额
"description": "Amount added to the product price in the cart and at checkout"
}
响应(节选,新建的定义在 data.metafield_definition):
{
"code": "Success",
"data": {
"metafield_definition": {
"id": "705345636089727677",
"namespace": "custom",
"key": "markup",
"name": "Markup",
"owner_resource": "product",
"type": "integer"
}
}
}
定义属于店铺,每个店铺只需创建一次。完整字段见创建元字段定义。
之后由商家设置附加费:在店铺后台打开需要加收费用的商品的编辑页,在底部「元字段」里的 Markup 字段填入金额;不收费的商品留空。如果你的应用有自己的设置页,想让商家在那里填写,也可以由应用调用创建元字段接口写入。
第 2 步:创建函数扩展
在应用项目根目录下,创建一个名为 markup-price 的函数扩展:
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]] 块:
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() 换成下面的逻辑:
// 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、一个没有这个元字段时,函数收到的输入(节选):
{
"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": []
}
}
]
}
}
返回:
{
"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 步:发布应用
在应用项目根目录下发布应用。这一步会编译函数,把它和声明一起上传,并生成一个新的应用版本:
shoplazza app deploy
输出里函数那一项带有 input_query,也就是第 3 步的声明:
{
"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」。
在这次发布之前就装了应用的店铺,会继续运行它绑定时的函数版本,重新发布不会让它切换过去。要测试新的函数版本或修改后的声明,在测试店铺上卸载并重新安装应用。
验证
在测试店铺上:
- 把设置了
custom.markup = 5、标价 100 的商品加入购物车。商品行显示 105,标题不变; - 再加购一个没有这个元字段的商品,价格保持标价;
- 进入结账页,第一个商品行仍是 105,标题显示为
Markup Tee (+5); - 打开商品页,仍显示标价 100。
给购物车里已有的商品行增加数量,价格仍是加购时算出的价格。改了附加费或函数之后,先删掉这一行再重新加购,才能看到新价格。
结账页效果如下:

下一步
- Function 扩展开发指南:编译、发布、绑定,以及完整的输入输出约定
- 创建元字段定义:元字段定义接口的全部字段
- 按尺寸计价销售商品:按买家填写的尺寸计价