1. 页面概览
1.1 是什么
「函数注册」页面(页面内标题为 本体函数)是 LightFoundry 的 OntologyFunction(本体函数)注册与测试工作台,V5 Stage 5(B7-2)交付。页面 hint 给出定位:「OntologyFunction · 服务端业务函数(工作流条件 / 校验 / 编辑态派生值)」。
在 Foundry 中,工作流条件、数据校验、编辑态派生值这些场景需要「可复用的计算逻辑」:例如折后价 = 原价 × (1 - 折扣率)、满减金额计算、订单状态拼接等。如果每次都把表达式写死在各处,会重复且难以维护。本体函数就是把这类逻辑声明式注册到本体的函数表(fon_functions)中:一个函数 = 一个稳定的 name(api_name)+ 参数声明 params(JSON)+ 函数体 body(表达式),可被工作流条件/校验/编辑态派生值引用,也可以被其他函数递归调用(深度 ≤ 5)。
函数体使用表达式引擎语法(workflow.EvalExpression):{{param}} 引用参数、fn_name(arg1, arg2) 函数调用、+ - * / 运算、比较与逻辑运算,内置函数(len/abs/round/min/max/coalesce/if/ifnull/concat/upper/lower/now/date_trunc)与用户函数可混用。页面提供完整的函数列表、编辑器与测试面板——测试面板按 params 声明动态生成参数输入框,输入参数后即时求值(POST /ontology/functions/:name/test)。
1.2 核心价值
| 维度 | 说明 |
|---|---|
| 声明式注册 | 用 JSON 声明参数、用表达式写函数体,无需写 Go/Python 代码即可扩展业务计算 |
| 稳定标识 | name 为不可变的 api_name(^[a-z][a-z0-9_]*$),被引用后不可改名 |
| 参数类型化 | params 声明 name/type/required,测试面板按声明动态生成输入框 |
| 即时求值 | 「运行求值」把输入参数 POST 到 /ontology/functions/:name/test,秒级验证函数逻辑 |
| 递归复用 | 用户函数可被其他函数 body 以 fn_xxx(...) 调用(递归深度 ≤ 5,防死循环) |
| 灰度开关 | pure(纯函数)与 enabled(启用)开关控制函数是否可被表达式调用 |
1.3 一句话总结
2. 访问入口
2.1 路由与菜单
- 路由:
/foundry/functions - 路由名称:
FoundryFunctions - 路由 meta:
title: 函数注册,requiresAuth: true,挂在父路由/foundry(FoundryLayout)下 - 菜单位置:Foundry 左侧边栏「函数注册」(FoundryLayout 菜单项)
- 前端源码:
action/web/src/views/FunctionPage.vue - API 客户端:
action/web/src/api/client.js(通用 apiClient,/api/v1)
2.2 认证与权限
- 页面路由挂
requiresAuth: true,未登录访问重定向到/login。 - 数据请求走通用
apiClient(client.js):baseURL/api/v1,请求拦截器自动附带Authorization: Bearer <aip_token>;响应拦截器 401 时清理aip_token/aip_username并跳转/login。 - 后端
/ontology/functions/*挂 protected 语义(authMiddleware),未带有效令牌返回 401。 - 页面顶部注释说明「后端未就绪时优雅提示不白屏(loading + alert 错误提示,防御式取值)」——列表加载失败只弹错误提示,不阻断页面渲染。
2.3 端口与 API 前缀
- Foundry 后端端口:18081。
- API 前缀:
/api/v1(client.js 的 baseURL)。 - 完整请求示例:
GET /api/v1/ontology/functions、POST /api/v1/ontology/functions/:name/test。
3. 界面布局
页面为「左窄右宽」两栏布局(grid 380px + 1fr):
┌────────────────────────────────────────────────────────────────┐
│ 本体函数(页头 + hint:OntologyFunction · 服务端业务函数) │
│ [alert 操作结果提示条(可关闭)] │
├───────────────────┬────────────────────────────────────────────┤
│ 函数列表(左) │ 编辑器(v-if editing) │
│ (共 N 个) │ 编辑函数/新建函数 · name 为稳定标识 │
│ [+ 新建函数] │ 函数名 name * | 返回类型 return_type │
│ 表格: │ 描述 | 参数 params(JSON 数组) │
│ 名称(描述) │ 函数体 body *(表达式) │
│ 类型(return/pure) │ [纯函数(pure)] [启用(enabled)] │
│ 状态(启用/禁用) │ [保存] [取消] │
│ 操作(编辑|测试|删除)│ ────────────────────────────────────────── │
│ │ 测试面板(v-else-if testing) │
│ │ 测试函数 {name} [关闭] │
│ │ 参数输入框(按 params 动态生成) │
│ │ [运行求值] [清空] 结果:<code> │
│ │ ────────────────────────────────────────── │
│ │ 空态(v-else):选择左侧函数进行编辑或测试, │
│ │ 或点击「+ 新建函数」注册本体函数。 │
└───────────────────┴────────────────────────────────────────────┘
各板块职责:
- 函数列表(左栏):展示全部函数(名称 + 描述截断、返回类型、pure/impure 标签、启用/禁用状态),行内提供「编辑」「测试」「删除」;「+ 新建函数」打开编辑器。
- 编辑器(右侧):新建/编辑函数——函数名(编辑时禁用)、返回类型、描述、参数 JSON、函数体表达式、纯函数与启用开关,以及「保存」「取消」。
- 测试面板(右侧):按函数 params 声明动态生成参数输入框,填入参数后「运行求值」即时求值并展示结果;「清空」重置。
- 空态提示(右侧):未选择函数且不在编辑/测试时显示引导文案。
4. 交互元素详解
4.1 函数列表(左栏)
| 元素 | 含义 | 操作效果 | 触发后端调用 |
|---|---|---|---|
| 「+ 新建函数」按钮 | 打开新建表单 | 编辑中点击则收起表单;否则重置表单并打开编辑器,标题「新建函数」 | 无(打开表单) |
| 名称列 | 函数名(api_name) | 粗体展示 name;描述超 32 字截断加省略号 | — |
| 类型列 | return_type + pure | return_type 显示 tag-blue(缺省显示 any);pure 显示 tag-green,否则灰色 impure | — |
| 状态列 | enabled 开关 | 启用(term-approved)/ 禁用(term-deprecated)徽标 | — |
| 「编辑」按钮 | 打开编辑表单 | 回填函数字段(params 格式化 JSON),标题「编辑函数」,name 输入框禁用 | 无(打开表单) |
| 「测试」按钮 | 打开测试面板 | 关闭编辑器,按 params 生成参数输入框 | 无(打开面板) |
| 「删除」按钮 | 删除函数 | confirm 二次确认「确定删除函数"{name}"吗?引用它的计算属性将无法求值。」后删除 | DELETE /ontology/functions/:id |
4.2 编辑器(右侧)
| 元素 | 含义 | 必填与默认值 | 操作效果 | 触发后端调用 |
|---|---|---|---|---|
| 函数名 name * | 稳定标识(api_name) | 必填;小写字母开头的小写字母/数字/下划线;编辑时输入框禁用 | 新建时提交 name;编辑时不可改名(后端校验「function name(api_name)为稳定标识,不可修改」) | 写入 name |
| 返回类型 return_type | 函数返回值类型 | 可空,placeholder 如 number / text / bool | 类型标签展示用 | 写入 return_type |
| 描述 | 函数用途说明 | 可空 | 列表展示(32 字截断) | 写入 description |
| 参数 params(JSON 数组,可选) | 参数声明 | 可空;必填时须为 JSON 数组,如 [{"name":"amount","type":"number","required":true}] | 保存时前端先 JSON.parse 校验,非法提示「params JSON 非法」不提交 | 写入 params |
| 函数体 body *(表达式) | 表达式逻辑 | 必填;placeholder 如 {{amount}} * (1 - {{rate}} / 100) | 保存时后端做语法检查(CheckExpressionSyntax),不可编译则拒绝 | 写入 body |
| 纯函数(pure) | 是否纯函数 | checkbox,默认勾选 | 纯函数可被任意表达式引用 | 写入 pure(1/0) |
| 启用(enabled) | 是否可被调用 | checkbox,默认勾选 | 禁用后引擎注销该函数,调用被拒绝 | 写入 enabled(1/0) |
| 「保存」按钮 | 提交 | busy 时显示「提交中...」并禁用 | 新建提示「函数创建成功 id=xxx」/ 更新提示「函数已更新」 | POST /ontology/functions 或 PUT /ontology/functions/:id |
| 「取消」按钮 | 关闭编辑器 | — | 丢弃编辑内容 | 无 |
4.3 测试面板(右侧)
| 元素 | 含义 | 操作效果 | 触发后端调用 |
|---|---|---|---|
| 参数输入框 | 按 params 声明动态生成 | 每个参数一行 label(name + type tag + 必填 tag-red)+ 输入框;placeholder 按类型:number→数字、bool→true/false、其他→文本 | 收集参数值 |
| 「运行求值」按钮 | 提交求值 | testingBusy 时显示「求值中...」;结果展示 结果:<code>…</code>,失败红色展示 error | POST /ontology/functions/:name/test(body {args: {...}}) |
| 「清空」按钮 | 重置 | 清空全部参数、结果与错误 | 无 |
| 「关闭」链接按钮 | 关闭测试面板 | 退出测试状态 | 无 |
4.4 参数值转换规则(前端 runTest)
- 输入为空:required 参数填
null提交,可选参数跳过(不提交该键)。 - type=number:
Number(raw)转换。 - type=bool:
raw === 'true' || raw === '1'判断。 - type=json:
JSON.parse(raw)(解析失败进 catch 展示错误)。 - 其他类型:按字符串提交。
- 后端
namedArgsToVars:必选参数缺失报「必选参数 "x" 未提供」;可选参数缺失填 nil。
5. 后端关联
5.1 API 客户端
- 文件:
action/web/src/api/client.js(通用客户端,函数页复用) - baseURL:
/api/v1;超时 30000ms;Content-Type: application/json - 请求拦截器:自动附带
Authorization: Bearer <aip_token> - 响应拦截器:401 时清理
aip_token/aip_username并跳转/login - 函数页直接调用
apiClient.get/post/put/delete,读取data.data.functions/data.data.result
5.2 端点表
| 方法 | 路径 | 请求体 | 说明 |
|---|---|---|---|
| GET | /ontology/functions | — | 函数列表(按 name 字典序) |
| POST | /ontology/functions | FunctionInput | 创建函数(name 正则/冲突/重名/body 语法检查后落库并注册进表达式引擎) |
| GET | /ontology/functions/:id | — | 函数详情 |
| PUT | /ontology/functions/:id | FunctionInput | 更新(name 稳定标识不可改;body 语法检查) |
| DELETE | /ontology/functions/:id | — | 删除并从表达式引擎注销 |
| POST | /ontology/functions/:name/test | {args: {参数名: 值}} | 按参数名填充 vars 对 body 求值 |
5.3 响应结构
函数对象(functions 数组元素):
{ "id": "...", "name": "discount_amount", "description": "折扣后金额",
"params": [ {"name":"amount","type":"number","required":true},
{"name":"rate","type":"number","required":false} ],
"return_type": "number", "body": "{{amount}} * (1 - {{rate}} / 100)",
"pure": 1, "enabled": 1, "created_at": "...", "updated_at": "..." }
测试求值响应(data 含 name 与 result):
{ "code": 0, "data": { "name": "discount_amount", "result": 85 } }
5.4 关联模块表
| 后端包 | 职责 |
|---|---|
products/foundry/ontology/function_rest.go | REST 端点、Test 请求解析({args})、统一响应 |
products/foundry/ontology/function.go | FunctionService:CRUD、校验(name 正则/内置函数冲突/params 结构)、Test 求值、用户函数注册与递归求值 |
products/foundry/ontology/models_function.go | fon_functions 表模型、FunctionParam、AutoMigrate |
products/foundry/ontology/validate.go | objectNamePattern 等校验正则与错误结构 |
products/foundry/workflow | 表达式引擎:EvalExpression / CheckExpressionSyntax / RegisterUserFunction / MaxUserFunctionDepth |
5.5 关键机制
- name 正则:
^[a-z][a-z0-9_]*$——小写字母开头,后续为小写字母/数字/下划线;且不能与内置函数名冲突(IsBuiltinFunctionName)。 - body 语法检查:保存时后端
workflow.CheckExpressionSyntax(body)校验可编译,不可编译拒绝(前端提示「body 表达式不可编译」)。 - 用户函数注册:Create/Update/Delete/Test 后调用
registerUserFunctions,把库中已启用的函数同步注册进表达式引擎(重名覆盖),不再存在/已禁用的注销。 - 递归求值:函数 body 内可调用其他用户函数
fn_xxx(...);递归深度超过workflow.MaxUserFunctionDepth(≤5 层)被拦截,防死循环。 - 测试求值:
Test先按参数名填充 vars(必选缺失报错、可选填 nil),再EvalExpression(body, vars)求值;禁用函数直接报「函数 "x" 已禁用」。 - 计算属性联动(边界):
OntologyProperty.IsCalculated的 Formula 允许引用fn_xxx(...),但 semantic mapper 处计算列遇函数引用时降级为 NULL+标注(不在 SQL 翻译层实现用户函数,P0 范围:函数仅在工作流条件/校验/编辑态派生值中执行)。
6. 核心流程详解
6.1 主流程:注册函数 → 即时求值 → 发布复用
- 新建函数:点「+ 新建函数」,填写函数名 name(小写字母开头)、返回类型、描述、参数 JSON 与函数体表达式,勾选 pure/enabled,点「保存」。前端先校验 params JSON 合法性,后端校验 name 正则与 body 语法,成功提示「函数创建成功 id=xxx」。
- 编辑函数:列表点「编辑」回填字段;name 输入框禁用(稳定标识不可改),仅可改 description/params/return_type/body/pure/enabled;保存提示「函数已更新」。
- 即时求值:列表点「测试」打开测试面板,按 params 声明填入参数(可选参数留空按 null 处理),点「运行求值」提交
POST /ontology/functions/:name/test,页面展示结果:<code>…</code>。 - 发布复用:保存并启用后函数即注册进表达式引擎,可被工作流条件/校验/编辑态派生值以及其他函数 body 以
fn_xxx(...)调用。
6.2 函数体表达式要点
- 变量引用:
{{参数名}},如{{amount}}、{{rate}}。 - 运算:
+ - * /;比较与逻辑:== != > < && || !。 - 内置函数:
len abs round min max coalesce if ifnull concat upper lower now date_trunc;用户函数:fn_xxx(...)(自定义函数名直接调用)。 - 示例:
{{amount}} * (1 - {{rate}} / 100);if({{total}} > 1000, {{total}} * 0.9, {{total}});concat(upper({{city}}), {{table}})。 - 提示:body 中参数以
{{参数名}}引用(页面以字面量文本展示,避免与 Vue 插值冲突)。
6.3 删除流程与影响
点「删除」弹出 confirm 弹窗「确定删除函数"{name}"吗?引用它的计算属性将无法求值。」——确认后 DELETE /ontology/functions/:id,后端从表达式引擎注销该函数;引用它的计算属性将无法求值,删除前应确认没有其他表达式在引用。
6.4 参数必选语义
- params 声明
required: true的参数:测试面板显示红色「必填」标签;留空时前端提交null,后端namedArgsToVars对缺失的必选参数报「必选参数 "x" 未提供」。 - 可选参数留空:前端跳过不提交,后端填 nil;求值时 nil 参与表达式按空值处理(如 coalesce 可兜底)。
7. 权限与安全
- 认证:全部端点经 authMiddleware(JWT,aip_token),未授权 401。
- 稳定标识不可改:name 为稳定标识,更新接口校验「function name(api_name)为稳定标识,不可修改」,防止引用断裂。
- 表达式沙箱:函数体为声明式表达式(EvalExpression),不支持任意代码执行;递归深度受限防死循环;禁用函数调用被拒绝。
- 写操作防护:删除函数用
confirm二次确认并提示「引用它的计算属性将无法求值」;保存前前端校验 params JSON 与后端 body 语法双重把关。 - pure/enabled 开关:禁用(enabled=0)函数即从引擎注销,调用被拒;pure 标记供调用方判断是否有副作用。
8. 常见问题与排错
问题一:保存函数时报「name 须为小写字母开头的小写字母/数字/下划线」或「body 表达式不可编译」
现象:保存被拒绝并提示 name 或 body 相关错误。
原因:name 不符合 ^[a-z][a-z0-9_]*$(如大写开头、含横杠),或 body 表达式语法错误(括号不匹配、变量引用拼写错、函数名不存在)。
排查步骤:检查 name 是否全小写字母/数字/下划线;检查 body 中 {{参数名}} 是否与 params 声明一致;先用测试面板构造最小表达式验证语法;保存后看 alert 具体错误。
问题二:测试面板「运行求值」显示红色错误「必选参数 "x" 未提供」
现象:求值失败,提示必选参数未提供。
原因:params 声明了 required=true 的参数但未填写。
排查步骤:找到红色「必填」标签的参数并填入;或编辑函数把该参数 required 改为 false;确认测试面板参数输入框确实由 params 声明生成。
问题三:函数保存成功但被其他函数以 fn_xxx(...) 调用时报「用户函数 "x" 不存在」或「已禁用」
现象:调用方求值失败。
原因:调用时机早于注册、函数被删除/禁用,或表达式引擎注册表未刷新。
排查步骤:确认被调函数仍在列表且状态为「启用」;测试面板每次求值前后端会 registerUserFunctions 刷新注册表;若删除后仍有引用,需一并修正引用方的 body。
问题四:测试结果为 null 或与预期不符
现象:求值结果为空或数值不对。
原因:可选参数留空被按 null 处理、bool 输入不是 true/1、number 输入含非数字字符、表达式逻辑与预期不符。
排查步骤:核对参数类型——bool 输入 true 或 1、number 只填数字;用 coalesce 给可能为 null 的参数兜底;逐步简化 body 缩小范围。
问题五:进入页面函数列表一直显示「加载中...」或「暂无函数(后端可能未就绪)。」
现象:列表加载不出来或为空。
原因:GET /ontology/functions 失败(401/网络/后端未启动)或列表为空。
排查步骤:检查浏览器 Network 里 /api/v1/ontology/functions 的状态码;401 则重新登录;确认 foundry 后端 18081 已启动;后端未就绪时页面有 alert 错误提示且不白屏(防御式取值)。
9. 已知缺陷与边界
| 边界 | 说明 |
|---|---|
| name 不可改 | name 为稳定标识,创建后不可修改(更新接口强制校验) |
| 表达式能力 | 仅支持运算/比较/逻辑/内置函数/用户函数,不支持循环、数组迭代、外部数据源访问 |
| 递归深度 | 用户函数互相调用深度 ≤ 5(MaxUserFunctionDepth),超出报错 |
| SQL 翻译层 | 计算属性 Formula 引用用户函数时 semantic mapper 降级为 NULL+标注,不在 SQL 层实现用户函数 |
| 参数类型 | type 仅 text/number/bool/json 等在测试面板做转换;date/datetime 未单独转换(按文本提交) |
| 删除联动 | 删除函数不校验被引用方,引用它的计算属性/表达式将无法求值(仅前端 confirm 提示) |
| EditRecorder 派生 | 本批不接入执行器(无「Action 写回后计算派生属性值」钩子),后续有派生值钩子时复用 evalUserFunction |