值表达式 (Value Expressions)
在 Script 中,凡是需要值的位置(URL、请求 body、字段值、条件、筛选值、目标 id 等)都是下面三种形态之一。例外只有两个:Regex 的 pattern 与 Cache 的 key 只能写成字面量,其中的 { /pointer } 不会被替换为值。本文档说明这三种形态、值来自何处(上下文根),以及 WEEGLOO 数据特有的 Locale 映射规则。Statement 目录中的所有字段都遵循此规则。
三种形态
| 形态 | 规则 | 示例 |
|---|---|---|
| 引用 (reference) | 在上下文中解析字符串内的 { /json-pointer }。 | "{ /payload/fields/title }" |
| 字面量 (literal) | 不含 { /ptr } 的值(字符串、数字、布尔值、对象、数组)。原样使用。 | "draft", 42, true, { "a": 1 } |
| 运算与条件 (JsonLogic) | 以单个运算符作为键的对象。操作数本身又是值表达式(引用、字面量、嵌套)。运算符是否需要加 $,取决于所在位置。 | { "$+": [ "{ /vars/n }", 1 ] } |
这三种形态可以嵌套:在 JsonLogic 操作数中放入引用,把引用的结果再放入运算,即可这样组合。
数据位置与表达式位置:何时加 $
同一份 JSON 会因所在位置不同而被解读成不同的含义。区分的标准是该位置的键归谁所有。fields 的键是 Content Type 的字段 id,Http.body 的键是对方 API 的 schema,因此在这类位置上,cat 或 in 必须是字段名,而不是运算符。
| 位置 | 对应字段 | 解读方式 |
|---|---|---|
| 数据位置 | fields(ResourceCreate、ResourceUpdate、ResourcePatch)、Http.body、Return.value、SetVar.value、Cache.value、Cache.defaultValue | 不带 $ 的键始终是字段名。要使用运算就加上 $。 |
| 表达式位置 | If.condition、Loop.while、version | 整个值就是表达式。运算符写成 cat 或 $cat 都可以。 |
| 模板位置 | 其余全部(url、method、headers[].value、locale、order、over、target.sys.id、EmailSend 的各个字段,以及 Signature·Hash·Regex 的值字段) | 因为是字符串,所以只能放入 { /pointer }。 |
| 仅字面量 | Regex.pattern、Cache.key | 不是值表达式。写在 Regex.pattern 中的 { /pointer } 不会被替换,而会成为 pattern 的一部分。 |
规则只有两条。
- 在数据位置,不带
$的键始终是字段名。 要使用运算,就给运算符加上$。 - 一旦通过
$进入表达式,其内部就全都是表达式。 嵌套的运算符不需要$(加上也可以)。
拿不准时,请给所有运算符都加上
$。 在任何位置都是正确的。
// 数据位置:cat 是 Content Type 的字段名(不是拼接运算)
"fields": { "cat": { "en-US": "hello" } }
// 在数据位置做计算:只在边界加 $,其内部保持原样
"fields": { "tier": { "en-US": { "$if": [ { ">=": [ "{ /p/score }", 700 ] }, "gold", "silver" ] } } }
// 表达式位置:原样书写
"condition": { "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }需要以 $ 开头的字段名时:$$
像 JSON Schema 的 $ref、$schema 那样,键本身确实需要以 $ 开头时,请把 $ 写两次。"$$ref" 表示数据键 $ref。只有最前面的一个 $ 会被剥掉($$$ref 即 $$ref),而且只对键生效(值里面的 $ 保持原样)。
"body": { "$$ref": "#/components/schemas/Item", "topK": { "$min": [ "{ /payload/fields/k }", 50 ] } }会被拒绝的两种情况
以下两种情况不会被悄悄解读为另一种含义,而是会作为错误被拒绝。
$键与同一对象中的其他键并存时会报错。运算必须是该对象的唯一键,同级数据往外挪一层即可。- 无法识别的
$键会报错。$catt并不是名为$catt的字段。$命名空间是为运算符保留的。
在表达式位置,运算符名与同级键并存同样会报错({ "and": […], "or": […] })。那里没有“这是数据”这一层解读,而且所有对象都判定为真,若放任不管,条件就会悄悄变成恒为真。
引用:{ /json-pointer }
在花括号内放入 RFC 6901 JSON Pointer(必须以 / 开头)。花括号周围允许有空格({ /a/b } 与 {/a/b} 相同)。
单一指针与混合模板:类型规则
- 当整个字符串是单一指针时,会保持该值的原始类型(数字则数字,对象则对象,数组则数组)。
- 与字面量文本混合时,会拼接(concatenation)为字符串。
"{ /payload/fields/count }" // 若为数字值则保持数字原样(例如 42)
"{ /payload/fields/tags }" // 若为数组则保持数组原样
"page-{ /payload/fields/n }-of-10" // 字符串拼接 → "page-42-of-10"
"Bearer { /payload/fields/token }" // 字符串拼接 → "Bearer abc123"缺失值
- 当路径不存在或值为空时,单一指针会作为
null处理,混合模板会作为空字符串处理。
上下文根:值从何而来
{ /pointer } 的顶层段是下面七种之一。
| 根 | 内容 |
|---|---|
/payload | 调用时传入的 JSON payload(输入)。例如:{ /payload/fields/email } |
/rawPayload | 同一个输入,但保留为调用方发送的 body 字符串本身(解析之前)。例如:{ /rawPayload } |
/headers | 调用时传入的请求 HTTP 头。键为小写,每个名称对应单个值。例如:{ /headers/authorization } |
/now | 执行开始的时刻。{ /now/seconds }·{ /now/millis }·{ /now/iso } |
/<name> | 带有 name 的前置 statement 的结果。例如:{ /order/sys/id } |
/vars/<name> | 用 SetVar 声明的 script-scoped 可变变量。例如:{ /vars/total } |
/error | 仅在 Try 的 catch 块内使用。即捕获到的错误 { message }。例如:{ /error/message } |
除 /<name> 之外的六个名称(payload·rawPayload·headers·now·vars·error)是保留名,不能用作 statement 的 name。用了同名会覆盖那个根,因此在保存时就会被拒绝(公共字段的绑定名规则)。
/rawPayload:发送时的原始 body
/payload 是解析后的值,/rawPayload 是同一个 body 的原文字符串。两者指向同一样东西,却并不相同:把解析后的值重新变成字符串时,空白、数字写法、转义、重复键都会被规范化,无法回到发送时的字节。
因此,在发送的字节之上计算出来的值只能用 /rawPayload 处理。 最典型的就是支付服务商 Webhook 的签名校验(Signature)。平时取值引用则用 /payload。
调用 body 只接受 JSON 对象。body 为空视为没有 body;不是 JSON 对象时(损坏的 JSON、数组、标量、字面量 null)不执行,并予以拒绝(参见错误)。
/now:执行开始的时刻
/now 以三种形态保存本次执行开始的时刻。
| 指针 | 值 |
|---|---|
{ /now/seconds } | epoch 秒(整数) |
{ /now/millis } | epoch 毫秒(整数) |
{ /now/iso } | 与 sys.createdAt 相同的平台时刻表示字符串(UTC) |
- 一次执行只有一个时刻。 它不是读取时钟的 statement,而是在执行开始时埋入的值,所以两个语句不会看到不同的值。
Parallel的每个分支也继承同一个时刻。因为不是语句,也不计入 statement 数量。 - 没有选择时区的字段。 epoch 值在任何地方都是同一个数,
iso则是 UTC 表示。 - 用于校验 Webhook 的 replay window(签名中携带的时间戳距现在多少秒以内)。时间戳通常以字符串传入,但算术运算会将其转换为数字,因此可以直接比较。
// 签名中携带的时间戳是否在 5 分钟(300 秒)以内
{ "<": [ { "-": [ "{ /now/seconds }", "{ /sig/1 }" ] }, 300 ] }statement 结果的形态
带有 name 的 statement,其结果形态因类型而异。
| statement | 结果形态 | 引用示例 |
|---|---|---|
Http | { status, body } | { /resp/status }, { /resp/body/choices/0/message/content } |
ResourceCreate、ResourceRead(单条)、ResourceFind(单条) | 资源本身 | { /post/sys/id }, { /post/fields/title/en-US } |
ResourceForEach | (遍历中)name 为当前项 = 资源本身。仅在 onEach 内引用 | { /post/sys/id }, { /post/fields/title/en-US } |
ResourceCount | 匹配的条数(整数) | { /commentCount } |
ParseJson | 解析后的值本身(对象·数组·标量) | { /quote/items/0/price } |
Signature | Boolean(是否通过校验) | { /verified } |
Hash | 字符串(按所声明表示形式的摘要) | { /expectedSign } |
Regex | Match 为 Boolean。Capture 为数组(0=整个匹配,1 起为捕获组),没有匹配时为 null | { /isOrderId }, { /sig/1 } |
ResourceFind在没有匹配时绑定null。用{ "==": [ "{ /found }", null ] }来分支判断是否存在。ResourceRead(单条)在目标不存在时会报错(可用Try处理)。详情参见 Statement 目录的资源读取。- 读取 ServiceUser 时,结果就是会员资源本身(
{ /member/sys/id })。与 Content·Media 不同,其字段不是 Locale 映射,而是值本身。规则参见读取会员目录。
运算与条件:JsonLogic
需要计算或条件时,使用 jsonlogic.com 规范中的运算符对象。
- 数据访问统一使用
{ /ptr }引用,而非原生的var(dot-path)写法。引擎先解析操作数中的指针,再应用运算符。 - 运算符必须是该对象的唯一键。在数据位置,只有带
$的键才是运算;在表达式位置,无论带不带$都是运算(参见数据位置与表达式位置)。
运算符表
表中的名称是运算符 token。在数据位置使用时要在前面加
$(cat写作$cat)。在表达式位置两种写法都可以。
| 分类 | 运算符 | 含义与示例 |
|---|---|---|
| 条件 | if(别名 ?:) | { "if": [条件, 真值, 条件2, 真值2, …, 默认值] }。取第一个为真的条件对应的值;若都不满足,则取最后的默认值。 |
| 逻辑 | and、or | 短路求值。and 返回第一个 falsy(或最后一个),or 返回第一个 truthy(或最后一个),并以其值本身返回。 |
| 逻辑 | ! (not)、!! (to-bool) | { "!": x } 是对 truthy 取反,{ "!!": x } 是判断是否 truthy。存在性检查常用 !!。 |
| 相等 | ==、!= | 宽松比较(强制转换为数字后比较。"1"==1 为真)。 |
| 相等 | ===、!== | 严格比较(连类型也比较)。 |
| 比较 | <、<=、>、>= | 可链式:{ "<": [1,2,3] } 即 1<2 AND 2<3。无法数字化(NaN)时为 false。 |
| 算术 | + | 所有操作数之和。 |
| 算术 | - | 一个操作数时取负,两个操作数时做减法。 |
| 算术 | *、/、% | 乘、除、取余。 |
| 聚合 | min、max | 操作数的最小值、最大值。 |
| 字符串 | cat | 把所有操作数拼接成字符串。 |
| 包含 | in | { "in": [needle, haystack] }。haystack 是字符串时判断子字符串,是集合时判断元素包含。 |
| 数组 | merge | 把多个数组或值扁平化为一个数组(用于累积收集)。 |
| 日期 | date | { "date": [值, 输出单位] }。把值规范化为可比较的时刻。输出单位为 millis(默认)、seconds、iso、day。参见日期规范化。 |
不支持数组遍历运算符(map、filter、reduce、all、some、none)。Script 通过 Loop 遍历数组(参见 Statement 目录的 Loop)。从列表中只挑出符合日期条件的项,也不是遍历要做的事,而是读取语句要做的事。给 ResourceFind 和 ResourceForEach 的 where 加上条件,服务器就会筛选后返回(可用的运算符见运算符列表)。
数字转换与示例
数字转换规则如下。数字保持原样,true 转为 1,false 转为 0,字符串会被解析(无法解析时计算会失败),null 转为 0。
日期字符串不是数字。 "2026-10-03" 无法被解析为数字,因此比较运算符不会报错,而是始终返回 false。要比较日期,请先用 date 规范化。
下面的片段以表达式位置为准。要放进数据位置(fields、Http.body、Return.value、SetVar.value)时,请给最外层运算符加上 $,内部的操作数保持原样。
{ "-": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] } // 余额 - 费用
{ "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] } // 余额 < 费用 → boolean
{ "and": [ { "<": [ "{ /a/body/risk }", 0.5 ] }, { ">=": [ "{ /b/body/score }", 700 ] } ] }
{ "cat": [ "id-", "{ /payload/sys/id }" ] } // "id-<uuid>"
{ "!!": "{ /found/sys/id }" } // 存在则为 true
{ "$merge": [ "{ /vars/ids }", [ "{ /row/sys/id }" ] ] } // 数组累积:SetVar.value 是数据位置,所以加 $
{ "if": [ "{ /payload/fields/next }", "{ /payload/fields/next }", "END" ] } // next 存在则取 next,没有则取 "END"日期规范化 (date)
比较运算符会先把操作数转换为数字,再进行比较。日期字符串不是数字,因此这样的比较不会报错,而是始终得到 false。换成 == 也解决不了。两边都不是数字时会直接比较文本,于是把同一时刻写成不同形式的 "2026-10-03" 与 "2026-10-03T00:00:00.000Z" 就会成为不同的值。日期请在比较之前用 date 规范化。
{ "date": [ 值, 输出单位 ] } // 输出单位可以省略
{ "date": "2026-10-03" } // 只传一个值时可以不写数组没有 before、after、equal 这样的专用运算符。规范化后的值是数字,因此直接使用现有的比较、算术、聚合运算符即可。
| 想要判定的内容 | 使用的表达式 |
|---|---|
| a 早于 b | { "<": [ { "date": a }, { "date": b } ] } |
| a 晚于 b | { ">": [ { "date": a }, { "date": b } ] } |
| 同一时刻 | { "==": [ { "date": a }, { "date": b } ] } |
| 同一天(忽略时刻) | { "==": [ { "date": [a, "day"] }, { "date": [b, "day"] } ] } |
| 在 from 与 to 之间 | { "<=": [ { "date": from }, { "date": x }, { "date": to } ] }(链式比较) |
| 一周之后 | { "date": [ { "+": [ { "date": x }, 604800000 ] }, "iso" ] } |
| 两个日期相差的天数 | { "/": [ { "-": [ { "date": a }, { "date": b } ] }, 86400000 ] } |
| 多个日期中最早的一个 | { "min": [ { "date": a }, { "date": b } ] } |
算术运算的结果又是毫秒数字,因此可以再放进 date 一次,以 iso 或 day 输出(上表中的“一周之后”)。
// 表达式位置:优惠券是否在有效期内。三个值的写法可以互不相同
{ "<=": [
{ "date": "{ /coupon/fields/startsAt/en-US }" },
{ "date": "{ /now/iso }" },
{ "date": "{ /coupon/fields/endsAt/en-US }" }
] }
// 表达式位置:HTTP Date 头是否在距现在 5 分钟(300 秒)以内
{ "<": [ { "-": [ "{ /now/seconds }", { "date": [ "{ /headers/date }", "seconds" ] } ] }, 300 ] }可读取的输入
下面这些值都会被读取为同一时刻。
| 格式 | 示例 |
|---|---|
| ISO-8601、RFC 3339 | 2026-10-03T00:00:00Z, 2026-10-03T00:00:00.000Z, 2026-10-03T09:00:00+09:00 |
| 省略了秒或小数部分的时刻 | 2026-10-03T00:00 |
在 T 的位置放了空格的时刻 | 2026-10-03 00:00:00 |
| 仅日期(按 UTC 零点读取) | 2026-10-03 |
RFC 1123(HTTP Date 头的表示形式) | Sat, 03 Oct 2026 00:00:00 GMT |
| epoch 数字与数字字符串 | 1790985600, 1790985600000, "1790985600" |
- 没有 offset 时按 UTC 读取。 offset 可以写成
+09:00、+0900、+09、Z,这些写法都接受。 - 解析是严格的。 即使位数正确,只要是实际不存在的日期(
2026-13-45),也会失败。 - epoch 按绝对值的大小来判断单位。 小于 100,000,000,000 时为秒,大于或等于该值时为毫秒。因此无论放入
{ /now/seconds }还是{ /now/millis },都会被各自正确读取。 - 认定为 epoch 的范围是绝对值大于或等于 100,000,000 且小于 100,000,000,000,000。 因为要按大小来判断单位,所以在两端都设了界限。超出该范围的数字不会被读成 1970 年,而是会失败。不带分隔符的日期
20261003、年份2026,以及表示没有值而传入的0都属于这种情况。
输出单位
第二个操作数决定输出形态。单位名称不区分大小写。
| 值 | 结果 | 使用场景 |
|---|---|---|
省略、millis | epoch 毫秒(数字) | 比较与算术 |
seconds | epoch 秒(数字)。不足一秒的部分会被舍弃 | 接受 epoch 秒的外部 API |
iso | 2026-10-03T00:00:00.000Z | 写入 Content 的 Date 字段 |
day | 2026-10-03(以 UTC 为准) | 同一天的比较、界面显示 |
给出列表中没有的名称时会失败,错误消息会列出可用的名称。
iso 输出与写入 Date 字段
Content 的 Date 字段在写入时只接受 yyyy-MM-ddTHH:mm:ss[.小数点]Z 这一种格式。T、秒和结尾的 Z 都必须有,小数部分可以写也可以不写,值按 UTC 读取。因此,把通过 payload 收到的 2026-10-03 或 2026-10-03T09:00:00+09:00 原样放进去,会因为值无效而被拒绝。date 的 iso 输出正好就是这种格式,所以在把收到的日期写入字段之前,先让它经过一次 date。
// 数据位置:把 payload 中的 "2026-10-03" 写入优惠券的到期日
"fields": { "endsAt": { "en-US": { "$date": [ "{ /payload/fields/endsAt }", "iso" ] } } }无法读取的值
下面三种情况会让该语句失败(status 400)。这是执行过程中发生的失败,因此可以用 Try 的 catch 局部处理。
- 第一个操作数缺失,或者引用没有找到值。
- 无法把值读取为日期。空字符串、只有空白的字符串、不是日期的字符串、不存在的日期、布尔值、对象,以及超出认定范围的数字都属于这种情况。
- 输出单位的名称不在列表中。
值缺失时不返回 null,这是有意设计的契约。null 在数字转换中会变成 0,从而被当作 1970 年来比较,于是缺少日期的检查不是失败,而是结果被反转。让超出有效期的优惠券通过,比让执行停下来更糟。
真假判定 (Truthiness)
if、and、or、!、!! 以及 If.condition、Loop.while 按以下规则判定真假。
- falsy:
null、false、数字0、空字符串""、空集合(空数组)。 - truthy:其余全部(非 0 的数字、非空的字符串和数组、所有对象)。
键也可以引用
像 fields 这样的映射,其键也支持 { /ptr } 引用。键会在运行时解析。
"fields": { "{ /payload/fields/fieldName }": { "en-US": "{ /payload/fields/fieldValue }" } }如果两个键解析为相同的值,就会发生冲突并导致引擎报错。
Locale 映射:Content 与 Media 特有的规则
WEEGLOO Content 与 Media 的每个字段都不是单一值,而是按 Locale 划分的映射(例如 balance 为 { "en-US": 1, "ko-KR": 10 })。因此读写时必须连同 Locale 一起处理。Media 同样,其 title 和 description(标量)、file(摄取指令)都是 Locale 映射。像 /payload 或 HTTP 响应这类并非 Content 或 Media 的 JSON 则与此规则无关(保持 schema 所定义的结构,是标量就是标量)。
读取
- 要取得标量,需连 Locale 一起指定:
{ /<name>/fields/<field>/<locale> }(例如{ /post/fields/title/en-US })。 - 不带 Locale 时,
{ /<name>/fields/<field> }会返回 Locale 映射的整个对象。 localized:false字段只存在于默认 Locale 存储桶中,因此用该默认 Locale 代码来读取。
写入 (ResourceCreate、ResourceUpdate、ResourcePatch 的 fields)
值是按 Locale 划分的映射 { "<locale>": <标量值表达式> }。与读取对称。
"fields": {
"title": { "en-US": "Hello", "ko-KR": "안녕" }, // 多个 Locale 就并列存储桶
"status": { "en-US": "paid" }
}ResourceCreate必须在所有要填充的字段中都包含 Space 默认 Locale 存储桶(default-locale 规则)。ResourceUpdate是整体替换。fields中不存在的字段和 Locale 会被移除(包括 file)。ResourcePatch只更新指定的字段和存储桶(其余字段和 Locale 保持不变)。- 用字面量
null删除:当值为字面量null时,会删除该 (field, locale) 存储桶(这是在 Patch 中清空特定 Locale 的标准做法)。""(空字符串)不是删除,而是设置为空值。值表达式({ /ptr })在运行时求值为 null 时不是删除,而是报错(不会静默吞掉 payload 的缺失)。删除只适用于字面量null。 Media的file:值不是标量,而是摄取指令{ "source": …, "encoding": "url"|"base64" }。摄取实际上做了什么,在 Statement 目录的 ResourceCreate 中讲解。localized:false字段只放入默认 Locale 存储桶。- Locale 代码(映射的键)也可以用
{ /ptr }引用(参见上文 键也可以引用)。用于创建动态 Locale。
locale 便利字段
在 ResourceCreate、ResourceUpdate、ResourcePatch 中提供 locale 时,引擎会自动把 fields 的每个值包装成 { <locale>: 值 } 存储桶。 也就是说只需提供标量即可。
// 下面两个是等价的
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"locale": "en-US", "fields": { "title": "Hello" } }
{ "type": "ResourceCreate", "resource": "Content", "contentType": { "sys": { "id": "ct_post" } },
"fields": { "title": { "en-US": "Hello" } } }如果在提供 locale 的同时又在值中嵌套了 Locale 映射({ "en-US": … }),就会变成 { <locale>: { "en-US": … } } 这样的双重嵌套(属于作者失误)。请统一为:用 locale 时只写标量,不用时只写显式的 Locale 映射。
where 与 order 中的 Locale
- 在
where和order中,fields.X的 Space 默认 Locale 会被引擎自动应用(与 CMA 查询一致)。 - 要指定特定的 Locale,请显式写成
fields.X.<locale>。
"where": { "fields.slug": { "eq": "{ /payload/fields/slug }" } } // 默认 Locale 的 slug
"where": { "fields.title.ko-KR": { "prefix": "안" } } // 特定 Locale错误
以下是违反值表达式规则时出现的错误码。这些检查在保存时进行;违反定义的其他静态约束的错误码在执行语义、约束与安全的错误中,调用时出现的错误码在端点的错误中。所有资源共通的错误码,请参见通用错误。
| 错误码 | 条件 |
|---|---|
WGL400056 | 在数据位置上,$ 运算键与同一对象中的其他键并存。 |
WGL400055 | 在数据位置上写了未被定义为运算符的 $ 键。 |
相关文档
- Statement 目录:使用值表达式的 25 种 statement 的字段与结果。
- 执行语义、约束与安全:执行顺序、错误、乐观锁、静态约束。
- Cookbook:组合值表达式的完整示例。
- Script 概述:顶层结构与一次执行可用的时间。
