# 织信低代码平台 表达式 SDK 文档

---

## ⚠️【表达式编写强制约束】——编写表达式前必须阅读本节

> **本文档描述的是织信平台自有的 DSL 表达式语言（基于 UEL），不是 JavaScript，不是 TypeScript，不是任何通用编程语言。**
>
> 编写织信表达式时，必须严格遵守以下禁止规则。**违反任何一条规则，均会导致表达式解析报错、运行失败。**

### 🔥 首要原则：`${}` 必须包裹整条表达式语句

`${}` 是"求值边界"，不是"参数插值占位符"。最常见的错误是把 `${}` 套在函数参数或运算项的位置上，这会导致表达式被当作字符串而不是被求值。**编写每一条表达式之前，先确认 `${}` 包住的是一条完整的 UEL 语句（函数调用整体、运算式整体、属性访问整体），而不是其中的一部分。**

```
❌  Array.of(${'1'})              输出字符串："Array.of(1)"，不是数组
✅  ${Array.of('1')}              求值为数组：[1]

❌  String.concat(${a}, ${b})     输出字符串："String.concat(...,...)"
✅  ${String.concat(a, b)}        求值为拼接后的字符串

❌  ${任务记录} != null            字符串比较，恒为 true
✅  ${任务记录 != null}            布尔值比较结果
```

如果写出来的表达式去掉 `${}` 后，剩余内容不是一条完整的 UEL 表达式（比如出现裸的 `Array.of(`、`String.concat(`），那一定是 `${}` 位置写错了。

### 🚫 绝对禁止事项（以下写法在织信表达式中均非法）

| 禁止的写法 | 原因 | 正确替代方案 |
|-----------|------|------------|
| `function(...) { ... }` | 织信表达式不支持定义函数 | 使用平台内置函数，如 `String.concat()` |
| `() => ...` | 不支持箭头函数 | 使用平台内置函数 |
| `var x = ...` / `let x = ...` / `const x = ...` | 不支持变量声明 | 直接写表达式 |
| `if (...) { ... } else { ... }` | 不支持 if/else 语句块 | 使用三元运算符 `A ? B : C` |
| `for (...)` / `while (...)` | 不支持循环语句 | 使用 Array 系列函数 |
| `new Date()` | 不支持 `new` 关键字 | 使用 `Date.sysdate()` |
| `new Array()` / `[...]` 数组字面量 | 不支持数组字面量 | 使用 `Array.of(...)` |
| `{ key: value }` 对象字面量 | 不支持对象字面量 | 使用 `Misc.newObject()` |
| `'A' + 'B'` 字符串用 + 拼接 | + 仅用于数值运算 | 使用 `String.concat('A','B')` |
| `${...}` 嵌套 `${}` | 禁止嵌套 | 去掉内层 `${}` |
| `Func(${arg})` 在参数位置写 `${}` | `${}` 必须包裹**整条表达式**，不能只包裹参数 | 改写为 `${Func(arg)}`，让 `${}` 包住整个函数调用 |
| `Func(${'a'}, ${'b'})` 多个参数各自包 `${}` | 同上，参数位置不允许 `${}` | 改写为 `${Func('a','b')}` |
| `'前缀' + ${expr} + '后缀'` 用 `${}` 拼接字符串字面量 | 字符串字面量不应在 `${}` 外，且 `+` 不能拼接字符串 | 改写为 `${String.concat('前缀',expr,'后缀')}` 或 `前缀${expr}后缀`（仅当作模板字符串） |
| `return ...` | 不支持 return 语句 | 表达式本身即为返回值 |
| `console.log(...)` | 不支持调试语句 | 无替代 |
| `parseInt(...)` / `parseFloat(...)` 全局函数 | 不支持 JS 全局函数 | 使用 `Misc.parseInt(...)` / `Misc.parseFloat(...)` |
| `Math.max(a, b)` 直接比较两值 | Math 函数仅接受单参数或列表 | 使用三元运算符 `a > b ? a : b` |
| `JSON.stringify(...)` | 不支持 JS 全局对象 | 使用 `Misc.jsonStringify(...)` |
| `.length` 属性访问 | 不支持属性语法访问长度 | 使用 `Array.length(list)` 或 `String.length(s)` |
| `array[0]` 下标访问 | 不支持数组下标语法 | 使用 `Array.get(list, 0)` |
| `obj.key` 访问不明来源对象属性 | 仅可访问上下文变量的属性 | 使用 `Misc.prop(obj, 'key')` |

### ✅ 编写表达式时必须遵守的规则

1. **表达式必须用 `${}` 包裹**，`${}` 之外的内容为普通字符串原样输出
2. **`${}` 必须包裹一条完整的表达式语句**，禁止只把 `${}` 套在函数参数、运算项等"半截语句"上。判断方法：把 `${}` 当成 "一对求值边界"——边界内的内容会被当作 UEL 表达式整体求值，边界外的内容是原样字符串。函数调用、运算、属性访问等必须**整体**位于 `${}` 内部。
   ```
   ❌ 错误：Array.of(${'1'})         → 输出字符串 "Array.of(1)"，不是数组
   ✅ 正确：${Array.of('1')}         → 求值为数组 [1]

   ❌ 错误：String.concat(${name}, '-', ${code})  → 输出字符串 "String.concat(张三, -, A01)"
   ✅ 正确：${String.concat(name, '-', code)}     → 求值为字符串 "张三-A01"

   ❌ 错误：${任务记录} != null          → 字符串 "[object] != null"，恒为 true
   ✅ 正确：${任务记录 != null}          → 布尔值 true / false

   ❌ 错误:  ${a} + ${b}                → 字符串拼接出 "1 + 2"
   ✅ 正确：${a + b}                     → 数值求和结果 3
   ```
3. **只能调用本文档中明确列出的函数**，严禁自行编造函数名称
4. **字符串拼接唯一方式**：`${String.concat(s1, s2, ...)}` ，禁止使用 `+`
5. **数组唯一构造方式**：`Array.of(item1, item2, ...)` ，禁止使用 `[...]`
6. **获取当前日期**：`Date.sysdate()`，禁止使用 `new Date()`
7. **条件分支唯一方式**：三元运算符 `A ? B : C`，禁止使用 if/else
8. **运算符必须写在 `${}` 内**，例如 `${任务记录 != null}` 而非 `${任务记录} != null`
9. **选项字段的原始值不加引号**，例如 `inProgress` 而非 `'inProgress'`

---

## 一、概述

织信低代码平台提供了一套 **表达式 SDK（Expression SDK）**，用于在以下场景中进行动态计算与逻辑控制：

- 工作流（BPMN）节点配置、审批人、流转条件
- 启动配置、实例名称、自动完成条件
- 自动化规则
- 表单、仪表盘、API 返回值
- 脚本与规则引擎

---

## 二、表达式执行规则

织信表达式采用 UEL（统一表达式语言） 语法，在语法和语义上类似于 JavaScript 表达式，**但并非 JavaScript 语法，存在严格的 DSL 约束，不支持任何 JavaScript 语句结构（函数声明、变量声明、循环、条件语句等）**。

### 2.1 表达式与模板规则

- 表达式使用 `${}` 包裹
- **`${}` 是"求值边界"**：边界内的内容会被作为一条完整的 UEL 表达式求值；边界外的内容会被当作普通字符串原样返回
- **`${}` 必须包裹整条表达式语句**，绝不能只把它套在函数参数、运算项等"半截语句"上：
   - ❌ 错：`Array.of(${'1'})` → 输出字符串 `"Array.of(1)"`（外层 `Array.of(...)` 不在 `${}` 内，是原样字符串）
   - ✅ 对：`${Array.of('1')}` → 求值为数组 `[1]`（整条函数调用都在 `${}` 内）
- 一个字符串中可以包含多个 `${}` 表达式，但每个 `${}` 内部都必须是独立完整的表达式
- 字符串拼接不支持使用 `+`，必须使用 `${String.concat('123','456')}` 返回 `'123456'`

### 2.2 表达式规则（强制）

**规则1：`${}` 必须包裹一条完整的表达式语句（最重要）**

`${}` 是"求值边界"，不是"插值占位符"。边界内的内容会被作为整条 UEL 表达式求值，边界外的内容是普通字符串。这意味着：**任何函数调用、运算、属性访问，都必须从头到尾整体写在 `${}` 内部**，绝不能只把 `${}` 套在参数或运算项上。

判断方法：删掉 `${}` 后剩下的内容，应该是一条**语法上完整的 UEL 表达式**；如果剩下的是 `Array.of(1)` 这样的"半成品函数调用"，但你只在参数位置写了 `${}`，整条表达式实际上是被当作字符串处理的。

```
❌ 错误：Array.of(${'1'})
   → 实际输出字符串："Array.of(1)"
   → 原因：Array.of(...) 部分在 ${} 之外，是原样字符串；只有 '1' 被求值

✅ 正确：${Array.of('1')}
   → 实际求值为数组：[1]
   → 原因：整条 Array.of('1') 都在 ${} 内，被当作函数调用求值

❌ 错误：String.concat(${name}, '-', ${code})
   → 实际输出："String.concat(张三, -, A01)"（字符串）

✅ 正确：${String.concat(name, '-', code)}
   → 实际求值为字符串："张三-A01"

❌ 错误：${a} + ${b}
   → 实际输出字符串："1 + 2"（两个值各自求值后用 + 拼成字符串）

✅ 正确：${a + b}
   → 实际求值为数值：3
```

**规则2：`${}` 内禁止再嵌套 `${}`**
```
❌ 错误：${Record.getRecordOptionName('task', 'priority', ${任务项.priority})}
✅ 正确：${Record.getRecordOptionName('task','priority',任务项.priority)}
```

**规则3：列表选择字段的原始值不加引号**
```
❌ 错误：'inProgress'
✅ 正确：inProgress
```

**规则4：运算符必须写在 `${}` 内**
```
❌ 错误：${任务记录} != null
✅ 正确：${任务记录 != null}
```

**规则5：只能使用文档中存在的函数，禁止自行编造**
```
❌ 错误：${new Date()}
✅ 正确：${Date.sysdate()}
```

**规则6：禁止使用任何 JavaScript 语句结构**
```
❌ 错误：${function() { return '值'; }}
❌ 错误：${() => '值'}
❌ 错误：${var x = 1; x + 1}
❌ 错误：${if (a > 0) { return a; } else { return 0; }}
✅ 正确：${a > 0 ? a : 0}
```

### 2.3 字符串拼接强制规则

在织信表达式中：

1. 运算符 `+` **仅允许**用于数值运算（Integer / Double）
2. **严禁**使用 `+` 进行字符串拼接，例如：
   - `${'A' + 'B'}` ❌
   - `${name + '-' + code}` ❌
3. 字符串拼接不支持使用\n换行：
   - 错误示例：${String.concat('a','\n','b')}
   - 正确示例：${String.concat('a','
     ','b')}
4. 所有字符串拼接**必须**使用：
   - `${String.concat('A','B')}` ✅
   - `${String.concat(name,'-',code)}` ✅

违反该规则的表达式将被视为**非法表达式**。

---

## 三、运算符

| 类型 | 符号 |
|------|------|
| 运算 | `+`（加法）, `-`（减法）, `*`（乘法）, `/`（除法）, `%`（取模） |
| 逻辑 | `&&`（并且）, `\|\|`（或者）, `!`（取反）|
| 关系 | `==`（等于）, `!=`（不等于）, `<`（小于）, `>`（大于）, `<=`（小于等于）, `>=`（大于等于）|
| 空值 | `null`（空值）|
| 条件 | `A ? B : C`，A 为 true 返回 B，否则返回 C |

> ⚠️ **注意**：`+` 运算符仅用于数值，不可用于字符串拼接。

---

## 四、保留关键字

```
and  or  not  true  false  null  empty  div  mod  in  matches
eq  ne  lt  gt  le  ge  class
```

> ⚠️ **以上关键字不可作为变量名使用**。

---

## 五、除 0 异常

在除法操作中，如果除数是 0 会抛出异常。

---

## 六、数组（Array）使用强制规则

### 6.1 数组字面量【绝对禁止】

在织信表达式中，**严禁**使用数组 / List 字面量语法：

```
❌ ['a']
❌ ['a','b']
```

上述写法在解析阶段会**直接报错**。

### 6.2 正确的数组构造方式（唯一允许）

```
✅ Array.of('a','b')
```

### 6.3 示例

```
❌ 错误：${Array.first(User.usersWithRole(['researcher']))}
✅ 正确：${Array.first(User.usersWithRole(Array.of('researcher')))}
```

---

## 七、函数调用

织信提供了 `Math` `Array` `Date` `Misc` `String` `User` `Encode` `Record` 等对象来处理数学运算、数组、日期等方面的函数运算。函数调用过程如果发生异常，系统会回滚当前事务。

示例：
```
${Math.abs(-100)}  // 返回 100
```

> ⚠️ **严禁调用本文档中不存在的函数。** 所有函数调用必须以 `对象名.函数名(参数)` 的形式书写，不存在全局函数（如 `parseInt()`、`isNaN()`、`Object.keys()` 等均非法）。

---

### 函数 - Array 数组集合函数

#### Array.of(...item)
将多个元素转化为数组

```
Array.of('x', 2, 3, 4)  // ["x",2,3,4]
```

#### Array.join(array, separator)
按照 separator 将 array 数组的内容拼接在一起

| 参数 | 类型 | 描述 |
|------|------|------|
| array | `Array` | 需要拼接的数组 |
| separator | `String` | 分隔符 |

**返回值**：类型 `String`，返回拼接的字符串

#### Array.length(list)
返回 list 的元素个数

#### Array.get(list, key)
获取集合中的一个元素。如果是 List 集合则 key 是 0~(len-1) 的索引整数；如果是 Map 集合则 key 是键值对的 key。超出范围将抛出异常。

#### Array.set(list, key, value)
设置集合中的元素值

#### Array.add(list, item)
向集合 list 的尾部添加元素 item

| 参数 | 类型 | 描述 |
|------|------|------|
| list | `Array` | 需要添加元素的集合 |
| item | `Object` | 要添加的元素 |

**返回值**：类型 `Array`，返回添加了元素后的集合

#### Array.remove(list, item)
将集合中的元素移除，如果集合是 Map 类型则 item 是 key

| 参数 | 类型 | 描述 |
|------|------|------|
| list | `Array` 或 `Map` | 需要移除元素的集合 |
| item | `Object` 或 `String` | 要移除的元素或元素的 key |

#### Array.removeAll(list, item)
将集合中的元素批量移除，如果集合是 Map 类型则 item 是 key

#### Array.isEmpty(list)
判断 list 是否是空集合；如果 list 为 `null` 或者长度为 0 返回 `true`，否则返回 `false`

#### Array.isNotEmpty(list)
判断 list 是否不是空集合；如果 list 为 `null` 或者长度为 0 返回 `false`，否则返回 `true`

#### Array.contains(list, item)
判断 list 中是否包含 item

#### Array.containsAny(list1, list2)
判断 list1 中是否包含 list2 中的任意元素。当 list1 为 null 时始终返回 `false`，当 list2 为空时始终返回 `true`

#### Array.containsAll(list1, list2)
判断 list1 中是否包含 list2 中的所有元素。当 list1 或 list2 为空时始终返回 `false`

#### Array.map(list, key)
返回 list 中 key 属性的列表

#### Array.props(list, props)
返回 list 中元素的 props 列表组成的新对象列表

#### Array.transform(list, mapping)
返回 list 中元素按照 mapping 映射组成的新对象列表

#### Array.concat(list1, list2)
返回 list1 和 list2 拼接在一起的新列表

#### Array.sort(list, key)
将 list 按照 key 属性排序

#### Array.distinct(list)
将 list 中的元素去除重复项后返回

#### Array.reverse(list)
将 list 反转后返回

#### Array.repeat(element, count)
生成一个每一项是 element 且长度为 count 的数组

#### Array.sublist(list, fromIndex, toIndex)
返回 list 的 fromIndex 到 toIndex 的部分

#### Array.filter(list, key, value)
从列表 list 中过滤属性 key 的值等于 value 的项

#### Array.shift(list)
移除 list 头部的元素并返回被移除的元素

#### Array.pop(list)
移除 list 尾部的元素并返回被移除的元素

#### Array.sum(list)
对元素为数字类型的 list 进行求和

#### Array.avg(list)
对元素为数字类型的 list 进行求平均值

#### Array.max(list)
对元素为数字类型的 list 求最大值

#### Array.min(list)
对元素为数字类型的 list 求最小值

#### Array.first(list)
返回 list 中的第一个元素

#### Array.last(list)
返回 list 中的最后一个元素

---

### 函数 - Context 上下文函数

上下文函数，表示在执行过程中的环境参数。

#### Context.userId()
返回当前操作的用户 ID

#### Context.appId()
返回当前的应用 ID

#### Context.appEnvProp(propKey)
返回当前应用的环境变量

#### Context.httpHeaders()
返回当前操作的 HTTP Header

#### Context.clipboardData()
返回当前应用剪切板中存储的数据

> 应用剪切板需通过以下方式设值：自动化步骤"设置应用剪切板"或控件"设置应用剪切板数据"

#### Context.weworkAccessToken()
返回企业微信 AccessToken

#### Context.dingtalkAccessToken()
返回钉钉 AccessToken

#### Context.feishuAccessToken()
返回飞书应用 AccessToken

#### Context.feishuTenantAccessToken()
返回飞书租户 AccessToken

#### Context.requestIp()
返回当前请求的 IP 信息

#### Context.hasAppPerm(permKey)
判断当前用户是否有标识符为 permKey 的应用权限

#### Context.hasModulePerm(moduleKey, permKey)
判断当前用户是否有模块标识符为 moduleKey 且权限标识符为 permKey 的模块权限

---

### 函数 - Date 日期函数

> ⚠️ **严禁使用 `new Date()`，获取当前日期必须使用 `Date.sysdate()`**

#### month 值范围

| 月份 | 值 |
|------|---|
| 1月 | 0 |
| 2月 | 1 |
| 3月 | 2 |
| 4月 | 3 |
| 5月 | 4 |
| 6月 | 5 |
| 7月 | 6 |
| 8月 | 7 |
| 9月 | 8 |
| 10月 | 9 |
| 11月 | 10 |
| 12月 | 11 |

#### day_of_week 值范围

| 天 | 值 |
|----|---|
| 星期日 | 0 |
| 星期一 | 1 |
| 星期二 | 2 |
| 星期三 | 3 |
| 星期四 | 4 |
| 星期五 | 5 |
| 星期六 | 6 |

#### Date.sysdate()
返回当前日期和时间

#### Date.now()
返回当前时间的 UNIX 时间戳

#### Date.newDate(year, month, day, hour, minute, second, millisecond)
返回指定的日期

| 参数 | 类型 | 描述 |
|------|------|------|
| year | `Integer` | 年份，非必填，默认当前年份 |
| month | `Integer` | 月份，非必填，默认 1月（值为0） |
| day | `Integer` | 天，非必填，默认 1 |
| hour | `Integer` | 小时，非必填，默认 0 |
| minute | `Integer` | 分钟，非必填，默认 0 |
| second | `Integer` | 秒，非必填，默认 0 |
| millisecond | `Integer` | 毫秒，非必填，默认 0 |

#### Date.dateSet(d, type, value)
将日期 d 中 type 指定的部分设置为 value

#### Date.dateAdd(d, type, diff)
计算日期 d 按照类型 type 加上 diff 的日期

| 参数 | 类型 | 描述 |
|------|------|------|
| d | `Date` 或 `Long` | 需要计算的日期或 UNIX 时间戳 |
| type | `String` | 运算类型 |
| diff | `Integer` | 增加或减少的值，正数为增加，负数为减少 |

type 取值：`year`（年）, `month`（月）, `day_of_year`（天）, `day_of_month`（月天数）, `day_of_week`（周天数）, `hour`（小时）, `minute`（分钟）, `second`（秒）, `millisecond`（毫秒）

#### Date.datePart(d, type)
返回日期 d 中 type 指定的部分

| 参数 | 类型 | 描述 |
|------|------|------|
| d | `Date` 或 `Long` | 需要计算的日期或 UNIX 时间戳 |
| type | `String` | 运算类型（同 dateAdd） |

#### Date.dateBefore(d1, d2)
判定日期 d1 是否在日期 d2 之前

#### Date.dateAfter(d1, d2)
判定日期 d1 是否在日期 d2 之后

#### Date.dateDiff(d1, d2)
计算两个日期之间的天数差值

#### Date.monthDiff(d1, d2)
计算两个日期之间的月份数差值

#### Date.weekDiff(d1, d2)
计算两个日期之间的周差值

#### Date.quarterDiff(d1, d2)
计算两个日期之间的季度差值

---

### 函数 - Encode 加解密函数

#### Encode.md5(s)
返回字符串 s 的 MD5 哈希

#### Encode.urlEncode(str)
将字符串 str 进行 URL 编码

#### Encode.urlDecode(str)
将字符串 str 进行 URL 解码

---

### 函数 - Math 数学运算函数

> ⚠️ **严禁使用 JavaScript 的 `Math` 全局对象用法（如 `Math.max(a,b)` 比较两值），织信 Math 函数参数与 JavaScript 不同，请严格参照本文档。**

#### Math.abs(x)
返回 x 的绝对值

#### Math.pow(d1, d2)
返回 d1 的 d2 次方

#### Math.ceil(x)
返回大于或等于 x 的最小整数

#### Math.floor(x)
返回小于或等于 x 的最大整数

#### Math.random()
返回 0 到 1 之间的随机数

#### Math.sqrt(x)
返回 x 的平方根

#### Math.round(n, digits)
返回数字 n 按照保留位数 digits 四舍五入的值

---

### 函数 - Misc 工具函数

> ⚠️ **严禁使用 `JSON.stringify()`、`JSON.parse()` 等 JavaScript 全局方法，必须使用 `Misc.jsonStringify()` 和 `Misc.jsonParse()`。**

#### Misc.jsonStringify(obj)
将对象 obj 转换为 JSON 字符串

#### Misc.jsonParse(str)
将字符串 str 转换为 JSON 对象

#### Misc.parseFloat(str)
将字符串 str 转换为小数

#### Misc.parseInt(str)
将字符串 str 转换为整数

#### Misc.timestampToDate(timestamp)
将 UNIX 时间戳数字值转换为日期类型

#### Misc.dateToTimestamp(date)
将日期类型转换为 UNIX 时间戳数字值

#### Misc.formatDate(date, fmt)
将变量 date 按照格式 fmt（如 `yyyy-MM-dd HH:mm:ss,SSS`）转换为字符串

#### Misc.parseDate(str, fmt)
将 str 按照格式 fmt（如 `yyyy-MM-dd HH:mm:ss,SSS`）转换为日期

#### Misc.host()
返回系统的首页地址

#### Misc.pinyin(str)
返回字符串 str 的中文拼音

#### Misc.shortPinyin(str)
返回字符串 str 的每个中文汉字拼音的首字母

#### Misc.expectNotNull(obj, message)
判定对象是否为空，为空则抛出提示为 message 的异常，否则返回对象

#### Misc.expectFirst(array, message)
返回 array 第一个元素。如果 array 为空，则抛出提示为 message 的异常

#### Misc.expectLast(array, message)
返回 array 最后一个元素。如果 array 为空，则抛出提示为 message 的异常

#### Misc.invokeScript(script, func, ...args)
调用脚本中的函数

#### Misc.invokeAutomatic(automaticId, ...args)
调用自动化程序

#### Misc.httpGet(url)
通过 GET 方式访问 url 地址，返回请求的内容

#### Misc.prop(object, key)
取出对象 object 中属性 key 的值

#### Misc.props(object, props)
取出对象 object 中属性 props 列表的新对象

#### Misc.transform(object, mapping)
返回 list 中元素按照 mapping 映射组成的新对象列表

#### Misc.appId()
获取所在应用的 ID

#### Misc.getAppIdByKey(key)
通过应用标识符查询团队下的应用 ID

#### Misc.attachmentURL(tableKey, fieldKey, value)
获取附件访问的链接地址

| 参数 | 类型 | 描述 |
|------|------|------|
| tableKey | `String` | 数据表 ID |
| fieldKey | `String` | 字段 ID |
| value | `String` | TableAttachment 中的 id 属性 |

#### Misc.appResURL(appResId)
获取应用资源库的资源地址

#### Misc.websiteResURL(moduleKey, filePath)
获取应用网站模块的资源地址

#### Misc.eval(expression, context)
使用上下文 context 变量执行表达式引擎替换模板中的数据

| 参数 | 类型 | 描述 |
|------|------|------|
| expression | `String` | 表达式字符串 |
| context | `Object` | 上下文变量 |

示例：
```
Misc.eval('name', {"name":"informat"})           // 'name'
Misc.eval('${name}', {"name":"informat"})        // 'informat'
Misc.eval('${detail.age}', {"name":"informat","detail":{"age":12}})  // 12
```

#### Misc.safesql(sql, params)
为带有占位符 `?` 的 sql 生成安全的完整 sql

#### Misc.uuid16()
生成一个 16 位的 uuid 字符串

#### Misc.uuid32()
生成一个 32 位的 uuid 字符串

#### Misc.newObject()
构建一个空对象

#### Misc.recordSql(sql, parameters)
执行 SQL 查询数据表记录列表，只允许执行 `SELECT` 语句，且只支持查询数据表模块的记录

| 参数 | 类型 | 描述 |
|------|------|------|
| sql | `String` | SQL 语句 |
| parameters | `Array<Object>` | 参数列表 |

**返回值**：类型 `Array<Object>`，查询的结果集

示例：
```
Misc.recordSql('select id,name from task where status=?', Array.of('1'))
```

返回值示例：
```json
[
  {"name": "名称", "id": "pht21vzu5ozps"},
  {"name": "333", "id": "1"}
]
```

---

### 函数 - Record 数据表函数

#### Record.getById(tableId, recordId)
返回查询的数据表记录信息

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| recordId | `String` | 记录 ID |

**返回值**：类型 `Object`，数据表记录信息，不存在则返回 `null`

#### Record.getFieldValue(tableId, recordId, fieldId)
返回数据表记录的字段值

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| recordId | `String` | 记录 ID |
| fieldId | `String` | 字段标识符 |

**返回值**：类型 `Object`，字段值；记录不存在返回 `null`，属性不存在返回 `null`，数据表不存在则抛出异常

#### Record.getByField(tableId, fieldId, opt, value)
根据单个字段过滤数据表记录列表

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| fieldId | `String` | 过滤的字段 |
| opt | `String` | 操作符，参见 `ConditionOpt` |
| value | `Object` | 过滤条件的值 |

**返回值**：类型 `Array<Record>`，最多返回 10000 条；无满足条件的数据返回空数组，数据表不存在则抛出异常

#### Record.getByFields(tableId, conditions)
根据多个字段过滤数据表记录列表

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| conditions | `Array` | 过滤条件 |

**返回值**：类型 `Array<Record>`，最多返回 10000 条

#### Record.getRecordOptionName(tableId, fieldId, value)
返回单个选项值的名称

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| fieldId | `String` | 字段的标识符 |
| value | `String` | 选项值 |

**返回值**：类型 `String`，选项值的名称；选项值不存在则返回传入的选项值；数据表不存在则抛出异常

#### Record.getRecordOptionNames(tableId, fieldId, valueList, join)
返回多个选项值的名称并拼接在一起

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| fieldId | `String` | 字段的标识符 |
| valueList | `Array<String>` | 选项值列表 |
| join | `String` | 拼接的字符串 |

**返回值**：类型 `String`

#### Record.getRecordOptions(tableId, fieldId)
返回选项值列表

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| fieldId | `String` | 字段的标识符 |

**返回值**：类型 `Array<Option>`；字段不是`列表选择`/`树形选择`/`级联选择`则返回 null；数据表不存在则抛出异常

#### Record.getRelationList(tableId, fieldId, recordId)
返回查询的数据表关联列表字段的值

| 参数 | 类型 | 描述 |
|------|------|------|
| tableId | `String` | 数据表的标识符 |
| fieldId | `String` | 字段的标识符 |
| recordId | `String` | 记录 ID |

**返回值**：类型 `Array<Record>`；字段不是`关联列表`则返回空数组；数据表不存在则抛出异常

---

### 函数 - String 字符串函数

> ⚠️ **严禁使用 `+` 进行字符串拼接，必须使用 `String.concat()`。严禁使用 `.length` 属性，必须使用 `String.length(s)`。**

#### String.upper(s)
将字符串的所有字母变成大写

#### String.lower(s)
将字符串的所有字母变成小写

#### String.concat(s1, s2, ...)
将多个字符串合并为一个字符串（**这是唯一合法的字符串拼接方式**）

#### String.lpad(s1, len, s2)
在字符串 s1 的开始处填充字符串 s2，使字符串长度达到 len

| 参数 | 类型 | 描述 |
|------|------|------|
| s1 | `String` | 要填充的字符串 |
| len | `Integer` | 总长度 |
| s2 | `String` | 填充字符 |

#### String.rpad(s1, len, s2)
在字符串 s1 的结尾处填充字符串 s2，使字符串长度达到 len

| 参数 | 类型 | 描述 |
|------|------|------|
| s1 | `String` | 要填充的字符串 |
| s2 | `String` | 填充字符 |
| len | `Integer` | 总长度 |

#### String.trim(s)
移除字符串 s 左右的所有空白字符

#### String.replace(s, s1, s2)
用字符串 s2 替代字符串 s 中第一次出现的字符串 s1

#### String.replaceAll(s, s1, s2)
用字符串 s2 替代字符串 s 中所有出现的字符串 s1

#### String.substr(s, start, len)
截取从字符串 s 的索引位置 start 起的 len 个字符

#### String.substring(s, start, end)
截取字符串 s 从 start 到 end 位置的子串

#### String.indexOf(s, s2)
获取字符串 s 中字符 s2 首次出现的位置

#### String.lastIndexOf(s, s2)
获取字符串 s 中字符 s2 最后一次出现的位置

#### String.contains(s, s2)
判定字符串 s 中是否包含字符 s2

#### String.length(s)
获取字符串 s 的长度（**严禁使用 `.length` 属性访问**）

#### String.startsWith(s, s2)
判定字符串 s 是否以字符 s2 开头

#### String.endsWith(s, s2)
判定字符串 s 是否以字符 s2 结尾

#### String.match(regex, input)
使用正则表达式 regex 验证 input 是否符合要求

| 参数 | 类型 | 描述 |
|------|------|------|
| regex | `String` | 正则内容（暂不支持 `\w`、`\d` 等以 `\` 开头的正则简写） |
| input | `String` | 匹配的内容 |

#### String.isEmpty(s)
判断输入字符串 s 是否为空（null 或空白字符串均返回 true）

```
String.isEmpty('')     // true
String.isEmpty(' ')    // true
String.isEmpty(null)   // true
```

#### String.isNotEmpty(s)
判断输入字符串 s 是否不为空

```
String.isNotEmpty(' a ')  // true
String.isNotEmpty('')     // false
String.isNotEmpty(' ')    // false
String.isNotEmpty(null)   // false
```

#### String.html2text(s)
将字符串 s 的 HTML 内容转换为文本内容（使用 XSS 过滤器过滤后输出）

---

### 函数 - User 用户函数

#### User.usersWithRole(roleIdList)
返回拥有 roleIdList 角色中任意一个角色的用户 ID 列表

| 参数 | 类型 | 描述 |
|------|------|------|
| roleIdList | `Array<String>` | 角色的标识符数组（必须使用 `Array.of(...)` 构造） |

#### User.usersWithDepartment(departmentIdList)
返回属于 departmentIdList 部门的用户 ID 列表

| 参数 | 类型 | 描述 |
|------|------|------|
| departmentIdList | `Array<String>` | 部门标识符数组（必须使用 `Array.of(...)` 构造） |

#### User.superiorUsers(userId)
返回 userId 的上级用户 ID 列表

#### User.superiorUsersWithLevel(userId, level)
返回 userId 的连续多级上级用户 ID 列表

| 参数 | 类型 | 描述 |
|------|------|------|
| userId | `String` | 要查询上级的用户 ID |
| level | `Integer` | 要查询的上级层数 |

#### User.subordinateUsers(userId)
返回 userId 的下属用户 ID 列表

#### User.subordinateUsersWithLevel(userId, level)
返回 userId 的连续下属用户 ID 列表

| 参数 | 类型 | 描述 |
|------|------|------|
| userId | `String` | 要查询下属的用户 ID |
| level | `Integer` | 级数 |

#### User.leaderOfDept(departmentId)
返回单个部门负责人 ID 列表

#### User.leaderOfDeptWithLevel(departmentId, level)
返回连续上级部门负责人 ID 列表

#### User.leaderOfDeptList(departmentIdList)
返回多个部门的负责人 ID 列表

#### User.parentOfDept(departmentId)
返回部门的父部门 ID

#### User.parentOfDeptList(departmentId)
返回部门的父部门 ID 列表

#### User.childrenOfDept(departmentId)
返回单个部门所有下级部门 ID 列表（递归返回所有子节点）

#### User.childrenOfDeptList(departmentList)
返回多个部门的所有下级部门 ID 列表

#### User.directChildrenOfDept(departmentId)
返回直接下级部门 ID 列表

#### User.user(userId)
返回单个 UserInfo 用户信息

#### User.userInfo(userId)
返回单个 UserInfo 用户信息

#### User.deptList(departmentIdList)
返回 `Array<InformatDepartment>`，departmentIdList 部门的部门信息列表

#### User.dept(deptId)
返回单个 InformatDepartment 部门信息

---

### 函数 - T 国际化函数

#### T.t(key)
返回指定 key 的国际化文本

---

## 八、模型定义

### UserInfo 用户信息

| 字段 | 类型 | 说明 |
|------|------|------|
| userName | string | 用户名 |
| mobileNo | string | 手机号 |
| email | string | 邮箱 |
| oid | string | OID |
| remark | string | 备注 |
| id | string | 用户 ID |
| name | string | 用户名称 |
| avatar | string | 头像 |
| roleList | Array&lt;string&gt; | 角色列表 |
| departmentList | Array&lt;string&gt; | 所属部门列表 |
| leaderList | Array&lt;string&gt; | 直接上级列表 |
| companyRoleList | Array&lt;string&gt; | 团队角色列表 |
| weworkUserId | string | 企业微信账户 ID |
| dingtalkUserId | string | 钉钉账号 ID |
| feishuUserId | string | 飞书账号 ID |
| userInfo | object | 自动化扩展信息 |
| permissionList | Set | 权限列表 |

### InformatDepartment 部门

| 字段 | 类型 | 说明 |
|------|------|------|
| id | string | 部门唯一标识（OID） |
| name | string | 部门名称 |
| shortName | string | 部门简称 |
| remark | string | 部门描述 |
| parentId | string | 上级部门 ID（OID） |
| ownerList | List&lt;string&gt; | 部门负责人 ID 列表 |
| rowNumber | number | 排序号 |

---

## 分模块文档索引

各模块的详细函数文档位于 `./expression/` 目录下：

| 模块 | 文件 | 说明 |
|------|------|------|
| 概览 | `./expression/index.md` | 表达式总览与基本语法 |
| Array 数组 | `./expression/array.md` | 数组操作函数（of、contains、filter、map等） |
| Context 上下文 | `./expression/context.md` | 上下文函数（userId、appId、companyId等） |
| Date 日期 | `./expression/date.md` | 日期操作函数（now、format、addDays等） |
| Encode 编码 | `./expression/encode.md` | 编码解码函数（base64、md5、urlEncode等） |
| Math 数学 | `./expression/math.md` | 数学计算函数（abs、round、max、min等） |
| Misc 其他 | `./expression/misc.md` | 杂项函数 |
| Record 记录 | `./expression/record.md` | 数据记录操作函数 |
| String 字符串 | `./expression/string.md` | 字符串操作函数（concat、substring、replace等） |
| User 用户 | `./expression/user.md` | 用户与部门操作函数（usersWithRole、userInfo、userList等） |
| isDisable | boolean | 是否禁用 |
