P2 Foundry
标准值类型与本体函数:把字段口径管成规范
V5 把属性类型冻结为 13 类标准值类型(text/number/integer/decimal/currency/percentage/date/datetime/bool/enum/json/array<text>/array<number>),存量属性用两段式归一化升级;再把重复出现的计算逻辑注册成本体函数(13 个内置 + 用户注册 + 在线测试)。看完这 4 个故事,你就能把"字段口径"和"计算口径"都收敛到一处权威定义。
数据工程师
业务分析师
ValueType
本体函数
类型归一化
公式
共 4 个故事
能 / 不能速览
✅ 这个主题能做
- 13 类冻结值类型目录:GET /ontology/value-types 一次给全类型 / 校验规则 / SQL 类型(sqlite / pg 双方言)
- 两段式归一化:POST /ontology/value-types/normalize 先 dry-run 返回 diff,再 confirm 写库
- 升级规则自动化:text+enum→enum、number+单位→currency/percentage、precision 0/>0→integer/decimal
- 本体函数:13 内置(len/abs/round/min/max/coalesce/if/ifnull/concat/upper/lower/now/date_trunc)+ 用户注册 + 在线测试
- workflow 条件与校验复用同一表达式引擎(EvalExpression / EvalCondition)
⛔ 这个主题做不了
- 旧值永不重写:存量属性类型读取时规范化,存储原样保留
- 计算属性 Formula 引用 fn_xxx(...) 时查询降级为 NULL(用户函数不在 SQL 翻译层执行)
- 新类型在语义 mapper 按 text/number 降级并标注,翻译行为不变
- constraints 的 unique/format/range 存而不验(enum 枚举 / currency 单位 / array 元素类型才联动校验)
- 函数只在测试面板 / workflow 条件 / 校验中执行,编辑态派生值钩子未接入
适用角色
本主题面向两个角色:
- 数据工程师:盘点 / 升级属性类型、注册与测试本体函数、给计算属性配公式——是核心使用者。
- 业务分析师:消费新类型字段与函数口径,在 workflow 条件里复用表达式引擎。
平台管理员负责权限与审计;业务决策者通过统一后的指标与字段口径消费结论,不直接操作。
能力速览(能做什么)
13 类冻结值类型
text/number/integer/decimal/currency/percentage/date/datetime/bool/enum/json/array<text>/array<number>,顺序冻结;旧 5 类兼容,历史别名读取时规范化。
类型归一化
扫描存量属性建议升级:dry-run 返回 diff(old_type/new_type/reason)不落库,confirm 逐条写 data_type,只迁移列不重建版本快照。
值校验与 SQL 转换
ValidateValue 按基类校验(enum 校验枚举值、array 校验元素);SQLCastExpr 提供 decimal→NUMERIC、datetime→TEXT ISO8601 的 sqlite/pg 双方言 CAST。
本体函数
13 内置函数 + 用户函数注册(name 正则、params/body),表达式语法 {{param}} + fn(arg),注册自动同步进引擎,递归深度 ≤5。
计算属性与工作流
计算属性 Formula 可引用函数(查询降级 NULL+标注);workflow 条件 / Action 校验规则复用同一表达式引擎求值。
调整指南(怎么调整)
- 换更精确的类型:新属性直接选 13 类;存量属性先看值类型目录,再跑 normalize 的 dry-run 评审 diff。
- 让归一化命中更准:先给 number 属性补 constraints.unit(币种 / %)或 precision,再跑归一化,升级建议才更贴合。
- 注册函数:name 用语义化小写下划线(勿与 len/abs 等内置冲突);params 里分清必选 / 可选,测试面板先跑通再挂引用。
- 复用表达式:workflow 条件 / Action 校验规则直接写 fn_xxx(...),与测试面板同一求值器,改函数 body 全链路生效。
- 禁用函数:enabled=0 即从引擎注销,调用被拒;删除函数同样注销,先确认没有引用再删。
做得好的场景
值类型 + 函数把"口径管理"从口头约定变成系统资产,特别适合以下场景:
- 口径对齐:金额 / 百分比 / 时间戳在类型层面就分清楚,不再"一个 number 走天下"。
- 存量升级低风险:两段式归一化先看 diff 再落库,改错了也不动版本快照,随时再跑。
- 计算逻辑一处维护:含税金额这类公式注册成函数,报表 / 工作流 / 校验全部引用同一实现。
- 在线验证:函数测试面板即时求值,比"改完 SQL 跑一遍看结果"快得多。
限制与不足
以下是明确的边界,使用前先知道:
- 旧值不重写:存量属性 data_type 保留原样,读取时经 ParseValueType 规范化,别名不落库。
- 函数不参与 SQL 翻译:计算属性 Formula 引用 fn_xxx(...) 时语义查询降级 NULL+标注,用户函数只在 workflow 条件 / 校验 / 测试面板执行。
- 新类型降级:semantic mapper 只识别存量 5 类,新类型按 number/text 降级处理并标注,不改变翻译行为。
- 约束存而不验:unique / format / range 约束目前只存储不校验;enum / unit / array 元素类型才联动校验。
- 派生值钩子未接入:EditRecorder 尚无"Action 写回后计算派生属性值"钩子,函数执行器未接入派生场景。
场景故事
故事 1
数据工程师盘点 13 类标准值类型:一次看清全部口径
场景:类型盘点
角色:数据工程师
耗时:约 5 分钟
- 背景
- 张工刚接手电商分析的本体建模。order 对象的 total 还是泛泛的 number、status 还是 text,类型语义不精确——"金额"和"百分比"都是 number,"日期"和"时间戳"都是 text,口径全靠人记。他打开"本体工作台 → 值类型目录",看 GET /ontology/value-types 返回的 13 类冻结类型和校验规则。
- 传统做法对比
- 以前属性类型靠建表时随手写 varchar/float,金额 / 百分比 / 时间戳在类型层毫无区分,做聚合时要么混用要么靠约定。现在 13 类冻结枚举 + 每类校验规则(SQLType / SQLTypePG / ValidateRules)一次给全,前端类型下拉与后端目录同源,不会再出现"界面填 text、库里存 varchar"的错位。
- 角色
- 数据工程师(盘点与建模);业务分析师后续消费新类型字段。
- 操作步骤
-
- 打开"值类型目录"(GET /api/v1/ontology/value-types)
- 逐类看 label / description / sql_type / sql_type_pg / validate_rules
- 对照 order 对象的 total / status 属性,标记可升级项
- 确认旧值兼容:varchar→text、float→number 等历史别名读取时规范化,不写回存储
- 系统响应
- GET /api/v1/ontology/value-types 返回目录数组:
{
"code": 0,
"data": [
{ "type": "text", "label": "文本", "description": "任意字符串",
"sql_type": "TEXT", "sql_type_pg": "TEXT", "validate_rules": ["任意字符串"] },
{ "type": "decimal", "label": "小数", "description": "十进制小数,可用 constraints.precision 指定精度",
"sql_type": "NUMERIC", "sql_type_pg": "NUMERIC",
"validate_rules": ["数值", "constraints.precision 指定小数位"] },
{ "type": "currency", "label": "金额", "description": "货币金额",
"sql_type": "NUMERIC", "sql_type_pg": "NUMERIC",
"validate_rules": ["数值", "constraints.unit 必填(币种,如 CNY/USD)"] }
]
}
array 类型额外带 element_type(text / number)。
- 结果洞察
- 13 类类型顺序冻结(text/number/integer/decimal/currency/percentage/date/datetime/bool/enum/json/array<text>/array<number>),存量 5 类保持合法且映射稳定;array 元素仅限 text/number,currency/percentage 必带 unit,enum 必带枚举值。新类型在语义 mapper 按 number/text 降级并标注,既有翻译行为不受影响。
- 调整建议
- 新属性直接选精确类型;datetime/date 用 TEXT 存 ISO8601,SQLCastExpr 提供 sqlite/pg 双方言 CAST 片段;存量属性先看目录再跑归一化,别手工改表。
- 动手试一试
- 登录:admin / admin1,端口 18081。输入内容:GET /api/v1/ontology/value-types。预期结果:data 数组首项 type=text、label=文本;decimal/currency 带 NUMERIC sql_type;array 类型出现 element_type。
- 限制提示
- 旧值永不重写:读取时规范化、存储原样保留;unique/format/range 约束存而不验;新类型在语义 mapper 只按降级基类处理(不改变翻译行为)。
故事 2
两段式归一化:先 dry-run 看 diff,再 confirm 写库
场景:类型迁移
角色:数据工程师
耗时:约 8 分钟
- 背景
- 张工想升级 order 对象:total(number + precision=2)应改为 decimal,status(text 但带 constraints.enum)应改为 enum。他先调用归一化接口的 dry_run 模式——只返回 diff 不写库,逐条评审建议后再 confirm 落库,避免"拍脑袋改类型"。
- 传统做法对比
- 以前改类型靠手工 ALTER TABLE + 改一堆消费 SQL,改错影响面大、还没有地方预览;口径建议只能靠有经验的人口头提醒。现在 normalize 两段式:dry-run 返回每条建议的 old_type / new_type / reason,确认后 confirm 逐条写 data_type,只迁移列、不重建版本快照,低风险可反复跑。
- 角色
- 数据工程师(发起迁移);评审人(确认 diff 无误)。
- 操作步骤
-
- POST /api/v1/ontology/value-types/normalize,body 传 {"dry_run": true}
- 逐条看 diff 的 reason:text+enum→enum、number+百分比单位→percentage、number+币种 unit→currency、precision 0→integer、precision>0→decimal
- 确认建议无误后 POST body 传 {"dry_run": false}
- 到对象详情核对 total / status 的 data_type 已更新
- 系统响应
- dry-run 返回 diff 不落库:
POST /api/v1/ontology/value-types/normalize
{ "dry_run": true }
->
{ "code": 0, "data": {
"dry_run": true,
"diff": [
{ "property_id": 1, "object_type_name": "order",
"property_name": "total", "old_type": "number",
"new_type": "decimal", "reason": "precision>0(小数值域)" },
{ "property_id": 2, "object_type_name": "order",
"property_name": "status", "old_type": "text",
"new_type": "enum", "reason": "text+constraints.enum→enum" }
],
"applied": 0 } }
confirm(dry_run=false)时 applied 为实际写库条数。
- 结果洞察
- 升级规则是"建议型":text+constraints.enum→enum、number+百分比单位(%/percent/percentage/pct)→percentage、number+币种 unit→currency、precision==0→integer、precision>0→decimal。建议类型均满足自身约束联动(currency 需 unit、enum 需枚举值),写库后校验通过;计算属性不参与迁移。
- 调整建议
- 先 dry-run 把 diff 给评审人过目再 confirm;给 number 补 unit / precision 约束后再归一化,升级建议更贴合;迁移只改 data_type 不重建版本,改错可再跑。
- 动手试一试
- 登录:admin / admin1。输入内容:先 {"dry_run":true},再 {"dry_run":false}。预期结果:dry-run 返回 diff 且 applied=0;confirm 后 applied>0,对象详情里对应属性 data_type 已更新。
- 限制提示
- 只迁移列 data_type,不重建版本快照;计算属性不参与迁移;旧存储值永不重写;语义 mapper 按降级基类处理新类型(integer/decimal/currency/percentage→number、datetime/json/array→text)。
故事 3
注册本体函数 compute_tax,在线测试即时求值
场景:函数注册
角色:数据工程师
耗时:约 6 分钟
- 背景
- 财务口径要求按税率计算含税额,多张报表重复书写同一段公式。张工在"本体函数"页注册一个用户函数 compute_tax:声明两个参数 x(金额)、rate(税率),body 用表达式语法 round({{x}} * {{rate}}, 2),然后打开测试面板传参即时求值。
- 传统做法对比
- 以前同一段计算逻辑在每张报表 / 每个 Notebook 各写一份,口径稍微一变要全量改;公式对不对还要贴到外部工具里验。现在 13 个内置函数(len/abs/round/min/max/coalesce/if/ifnull/concat/upper/lower/now/date_trunc)+ 用户函数注册(name 正则、params/body),创建时校验表达式可编译,注册即同步进表达式引擎,测试面板即时求值。
- 角色
- 数据工程师(注册 / 测试);分析师与工作流后续引用函数口径。
- 操作步骤
-
- POST /api/v1/ontology/functions 创建 compute_tax:params 声明 x / rate(必选),body=round({{x}} * {{rate}}, 2)
- 创建时校验 name 正则与 body 可编译(CheckExpressionSyntax)
- POST /api/v1/ontology/functions/compute_tax/test,body 传 {"args":{"x":100,"rate":0.06}}
- 看返回的 result 即时求值结果
- 系统响应
- 创建成功返回函数记录:
POST /api/v1/ontology/functions
{ "name": "compute_tax", "description": "按税率计算税额",
"params": [ { "name": "x", "type": "number", "required": true },
{ "name": "rate", "type": "number", "required": true } ],
"return_type": "number", "body": "round({{x}} * {{rate}}, 2)" }
->
{ "code": 0, "data": { "id": "7f3a...", "name": "compute_tax",
"return_type": "number", "body": "round({{x}} * {{rate}}, 2)",
"pure": 1, "enabled": 1 } }
在线测试:POST /api/v1/ontology/functions/compute_tax/test
{ "args": { "x": 100, "rate": 0.06 } }
->
{ "code": 0, "data": { "name": "compute_tax", "result": 6 } }
- 结果洞察
- 函数创建 / 更新后服务自动把启用函数同步注册进表达式引擎,body 内可引用其他用户函数(递归求值深度 ≤5 防死循环);enabled=0 即禁用(引擎注销、调用被拒);必选参数缺失 / 实参多余 / 函数已禁用均返回 422。
- 调整建议
- name 用语义化小写下划线,别和 len/abs 等内置名冲突;必选 / 可选参数分清,可选参数缺失填 nil;先跑测试面板再挂引用,改 body 全体引用自动生效。
- 动手试一试
- 登录:admin / admin1。输入内容:创建 compute_tax(body=round({{x}} * {{rate}}, 2)),再 test {"args":{"x":100,"rate":0.06}}。预期结果:创建返回 id 与 enabled=1;test 返回 result=6。
- 限制提示
- 函数 name 为稳定标识不可修改;与 13 个内置函数冲突 422;禁用函数调用被拒 422;函数求值仅用于测试面板、workflow 条件 / 校验,EditRecorder 派生值钩子尚未接入。
故事 4
计算属性公式引用函数:知道降级边界,别被 NULL 骗到
场景:计算属性
角色:数据工程师 + 业务分析师
耗时:约 10 分钟
- 背景
- 张工给 order 对象加计算属性 tax_amount(is_calculated=true、formula 引用 fn_compute_tax),同时业务分析师想把 workflow 里"金额>100 才发通知"的条件改成用函数表达。两人一起验证表达式引擎在计算属性与工作流两处的行为差异。
- 传统做法对比
- 以前计算字段要么在 SQL 里写死公式、要么在应用层各算各的,工作流条件只能写死字面量;公式改了要到处同步。现在公式可引用用户函数(fn_xxx),workflow 条件 / Action 校验经 EvalExpression 复用同一表达式引擎——一处定义、多处消费。
- 角色
- 数据工程师(建计算属性);业务分析师(配 workflow 条件)。
- 操作步骤
-
- 先注册并测试 compute_tax 函数
- 在 order 草稿中加计算属性 tax_amount:data_type=number、is_calculated=true、formula=fn_compute_tax(amount, 0.06)(不能与 mapped_column 同设)
- 提交 review → merge,到对象查询看该列
- 在 workflow 条件里写 if(fn_compute_tax({{amount}}, 0.06) > 100, 1, 0) 并运行验证
- 系统响应
- 对象详情中的计算属性:
{
"name": "tax_amount", "display_name": "税额",
"data_type": "number", "is_calculated": true,
"formula": "fn_compute_tax(amount, 0.06)",
"mapped_column": ""
}
语义查询该对象时,含函数引用的计算属性列降级为 NULL 并标注:{ "code": 0, "data": {
"columns": ["order_id", "tax_amount"],
"rows": [[1, null]] } }
- 结果洞察
- Formula 引用 fn_xxx(...) 时,语义 mapper 识别函数引用并降级为 NULL + 标注(FormulaHasFunctionRef / CalculatedFnDegrade)——用户函数不在 SQL 翻译层执行,P0 只在 workflow 条件 / 校验 / 测试面板真实执行。想拿计算值,走函数测试面板或 workflow 条件求值,别依赖对象查询这一列。
- 调整建议
- 计算属性公式先用函数测试面板验证结果再挂引用;workflow 条件 / Action 校验规则里复用表达式引擎,函数 body 改了全链路生效;复杂计算尽量保持简单类型。
- 动手试一试
- 登录:admin / admin1。输入内容:先建 compute_tax 并测试,再给 order 草稿加计算属性 formula=fn_compute_tax(amount, 0.06),merge 后查询。预期结果:函数测试 result=6;对象查询该列返回 null(降级标注,非数据缺失)。
- 限制提示
- 计算属性遇函数引用在查询端降级 NULL,不在 SQL 翻译层执行用户函数;函数仅在 workflow 条件 / 校验 / 测试面板执行;EditRecorder 派生值钩子未接入,Action 写回后不会自动算派生值。
常见问题
13 类值类型里,array<text> 和 json 有什么区别?
array<text>/array<number> 是元素类型受限的数组(元素仅 text/number,JSON 序列化存储);json 接受任意可序列化的 JSON 值(对象 / 数组 / 标量)。校验时 array 按元素逐个校验,json 只要求可序列化。
归一化会不会改坏我已有的数据?
不会。normalize 只迁移列 data_type(写属性定义),不重建版本快照,也不改源表数据;建议类型均满足自身约束联动(currency 需 unit、enum 需枚举值),写库后校验通过。dry-run 永远不写库,可以先看 diff 再决定。
函数 body 里能调用其他用户函数吗?
能。用户函数注册表并发安全,body 内可用 fn_xxx(...) 调用其他用户函数,递归求值深度 ≤5(防死循环 / 自引用)。函数创建、更新后服务自动把启用函数同步注册进表达式引擎。
为什么对象查询里计算属性是 NULL?
当计算属性公式引用 fn_xxx(...) 用户函数时,语义 mapper 会识别函数引用并降级为 NULL + 标注——P0 不在 SQL 翻译层执行用户函数。想要计算结果,走函数测试面板或 workflow 条件 / Action 校验规则。
存量旧类型(varchar / float)会被自动改掉吗?
不会。旧值永不重写:存量属性 data_type 保留原样,读取时经 ParseValueType 规范化(varchar→text、float→number 等历史别名归一)。想真正落库,用 normalize 的 confirm 段迁移。
主题小结
一句话:值类型管"字段口径"(13 类冻结 + 两段式归一化),本体函数管"计算口径"(13 内置 + 用户注册 + 在线测试)。记住几个边界:旧值永不重写、计算属性引用函数查询降级 NULL、新类型按 number/text 降级、unique/format/range 存而不验。