con un clic
coding-guide
Uniplat 低代码平台编码规范速查。在 Uniplat 项目中进行任何开发任务时参考。
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Uniplat 低代码平台编码规范速查。在 Uniplat 项目中进行任何开发任务时参考。
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
读取并分析 PRD 需求文档,提取要点和任务拆分。在 dev-flow 流程的第 1 步自动加载。
编码实现方法论:垂直切片 + TDD 核心循环。在 dev-flow 流程的第 5 步自动加载。
生成开发总结文档,记录交付物、依赖、设计要点。在 dev-flow 流程的第 8 步自动加载。
编码实现方法论:垂直切片,逐步推进。
在指定子项目目录下创建 Uniplat 数据模型脚手架(JSON 元数据 + Groovy 钩子文件)
创建 Uniplat 领域服务(DomainService)
| name | coding-guide |
| description | Uniplat 低代码平台编码规范速查。在 Uniplat 项目中进行任何开发任务时参考。 |
| version | 1.1.0 |
本 skill 是 Uniplat 低代码平台(Java + Groovy + JSON)的编码规范。任何 Uniplat 项目的编码任务都应遵循本文规范。
JSON 配置(模型元数据、字段、Action、列表/详情视图)
+
Groovy 脚本(Validator/Behavior/Updator/Service/EventHandler)
=
可运行的业务系统
核心思想:Java 核心代码不写业务逻辑,所有业务通过 Groovy 钩子实现。
平台运行时由三个服务组成:Gateway(统一入口:认证/路由/限流)→ Configurator(配置中心:加载并版本化 JSON/Groovy 配置)→ Runner(运行引擎:执行业务与 Groovy 钩子)。开发者只写「JSON 模型 + Groovy 钩子」,由 Runner 加载执行。两个核心运行时对象:
Host.getInstance(),用它拿数据模型/用户/子项目等资源。本文只讲「怎么写模型/Action/服务」。三服务内部机制、配置加载、事件总线、调度等运维细节不在编码范围。
一个 Uniplat 工作区包含多个子项目,每个子项目是独立 Git 仓库:
v3-refrom/ # 工作区根目录
├── uniplat-main/ # 平台主模块(引擎 + 配置 + schemas)
│ └── config/schemas/ # JSON Schema 定义(开发参考)
├── uniplat_base/ # 基础领域子项目
├── uniplat_common/ # 公共领域子项目
├── biz/ # 业务工作区(含多个子项目)
│ ├── account/ # 子项目(独立 Git)
│ ├── biz_spview/ # 子项目(独立 Git)
│ └── ...
subproject_name/
├── .git/ # 独立 Git 仓库
├── Entrances.groovy # 入口菜单配置
├── consts/ # 常量定义(mapping、joint 等)
│ ├── base/common.json
│ └── common.json
├── models/ # 数据模型定义(核心)
│ ├── system/ # 模型分组(子目录)
│ │ ├── system_user.json
│ │ └── system_user.groovy
│ └── business/order/
├── services/ # 领域服务(自定义 API)
│ └── passport_api.groovy
├── monitors/ # 监控/调度任务
├── dashboard/ # 看板
└── process/ # 工作流
注意:test/ 目录是废弃机制,新项目不创建。
{
"database": "(host)",
"table": "system_user",
"modelDescription": "用户",
"key_field": "id",
"data_right": true,
"mapping_defs": [
{
"name": "status_mapping",
"mapping_values": [
{"key": "0", "value": "待审核"},
{"key": "1", "value": "正常"}
]
},
{
"name": "city_mapping",
"database": "(host)",
"sql": "SELECT id, name FROM uniplat_district WHERE is_del = 0"
}
],
"mapping_refs": [
{
"subproject": "uniplat_base",
"filename": "base/common.json",
"key": "$.mappings.uniplat_user_type_mapping"
}
],
"field_defs": [
{
"property": "id",
"label": "id",
"type": "number",
"format": "0",
"column": {"type": "BIGINT", "autoIncrement": true, "comment": "主键"}
},
{
"property": "status",
"label": "状态",
"type": "mapping",
"mapping": "status_mapping",
"column": {"type": "INT", "defaultValue": "0"}
}
],
"joint_defs": [
{
"name": "member_role",
"sql": "SELECT xm.id mid, group_concat(role.name) role_names FROM ...",
"key_field": "mid",
"joint_field": "mid",
"field_defs": [
{"property": "mid", "label": "成员"},
{"property": "role_names", "label": "角色名"}
]
}
],
"calculator_defs": [],
"references": [
{
"name": "owner",
"model": "system_user",
"relations": [{"referProperty": "uid", "referredProperty": "id"}]
}
],
"action_defs": [
{
"name": "insert",
"when": "1",
"label": "新增",
"container": "dialog",
"parameters": {
"server": [
{"property": "created_by", "result": "env.user_id.value"},
{"property": "created_time", "result": "env.now.value"}
],
"inputs": [
{"property": "name", "label": "名称", "type": "text", "required": true, "span": 12}
]
},
"behavior": "insert",
"forward": "refresh"
}
],
"dataFilters": [
{"field": "is_del", "value": "0"}
],
"list": {
"label": "用户列表",
"filters": [
{"label": "状态", "field": "status", "type": "enum", "mapping": "status_mapping"}
],
"actions": ["insert"],
"row_actions": ["update", "delete"],
"field_groups": [
{"label": "名称", "template": "{name}"},
{"label": "状态", "template": "{status}"}
],
"dataFilters": [
{"field": "status", "value": "1"}
],
"detail_action_visible": false
},
"detail": {
"title_template": "",
"actions": [],
"header": {"field_groups": [], "actions": []},
"pages": []
},
"modelVersion": false
}
| behavior 值 | 含义 | 是否需要 Groovy 方法 |
|---|---|---|
"insert" | 平台内置插入 | 不需要 |
"update" | 平台内置更新 | 不需要 |
"delete" | 平台内置删除 | 不需要 |
"myCustomMethod" | 自定义方法 | 需要 |
"" | 无行为(纯跳转) | 不需要 |
默认情况下,insert/update/delete 使用平台内置 behavior,不需要在 Groovy 中写方法。 只有需要自定义业务逻辑时才改为自定义方法名并写 Groovy。
action 上有两个条件表达式,含义不同:
| 字段 | 含义 | 不满足时 |
|---|---|---|
when | 该 action 是否显示 | 隐藏(看不见) |
enable(= enabled) | 该 action 是否启用/可点击 | 置灰但仍显示,且不执行该逻辑 |
enable与enabled是同一语义的两种拼写:action_defs[]里多写enable,list 动作引用 / intent / JSON Schema 里写enabled。二者可同时与when并列出现。
空值安全铁律:when / enable 表达式一旦引用 object,必须先 object != null && 守卫。因为在 on:"list"/on:"none"(列表级、无选中行)等场景 object 本身就是 null,直接 object.字段.value 会抛 NPE。
// ✅ 推荐:先守卫 object 非空,再判业务条件
{"enable": "object != null && object.getValue('Status') == 0"}
{"when": "object != null && object.getValue('CurrentStatus') == 0"}
{"when": "object != null && (object.Status.value==2 || object.Status.value==3)"}
// ✅ 常量条件无需守卫
{"when": "1"}
{"enable": "1"}
// ❌ 反面:object 可能为 null 时直接点属性 → NPE
{"enable": "object.Status.value == 0"}
字段取值的两种写法(都可用):
object.getValue('字段') —— 返回原始值,字段不存在返回 null,== 值 比较不 NPE(推荐用于条件);object.字段.value —— 直接属性访问,字段包装为 null 时会 NPE,个别可空字段可叠加 object.字段?.value。关联/joint 字段用 object.get('本地键#joint名.字段').value(# 分隔本地键与 joint名.字段),例如:
{"when": "object != null && object.get('id#balance_refund_order_record_joint.OrderId').value != ''"}
说明:参考项目里大量历史表达式直接写
object.字段.value无守卫——那是应被纠正的隐患,新写法一律先判object != null。
dataFilters 用于自动过滤数据,可出现在:
| 位置 | 作用范围 |
|---|---|
模型顶层 dataFilters | 对所有查询生效 |
list.dataFilters | 仅对列表查询生效 |
list.pages[].dataFilters | 对特定页签生效 |
常用模式:
{"field": "is_del", "value": "0"} // 软删除过滤
{"field": "xid", "value": "context.xid"} // 组织隔离
{"field": "valid", "value": "1"} // 有效数据过滤
{"field": "poid", "value": "org.passports.PASSPORT"} // 动态值
模型可以不绑定实体表,而是用 SQL 定义视图:
{
"table": "virtual_view_name",
"sql": "SELECT id, name, COUNT(*) as count FROM real_table GROUP BY id",
"database": "(host)",
"key_field": "id",
"field_defs": [...]
}
虚拟模型只支持查询,不支持 insert/update/delete。
joint_defs 用于 SQL JOIN 关联,将多表数据合并到一个模型:
"joint_defs": [
{
"name": "member_role",
"sql": "SELECT member_id mid, GROUP_CONCAT(role_name) roles FROM ... GROUP BY member_id",
"key_field": "mid",
"joint_field": "id",
"field_defs": [
{"property": "mid", "label": "成员ID"},
{"property": "roles", "label": "角色列表"}
]
}
]
访问 joint 字段:obj.get("member_role.roles").value
// package 声明可选:参考项目里有的模型 groovy 声明了 package(对应分组目录),有的不声明也能跑。
// 建议按分组目录声明以便组织,如 package models.system。
package models.system
import com.example.report.uniplat.engine.DO.DataObject
import com.example.report.uniplat.executor.ActionBehaviorContext
import com.example.report.uniplat.executor.ActionValidatorContext
import com.example.report.uniplat.executor.BehaviorResult // 自定义 behavior 返回类型(常用)
import com.example.report.uniplat.executor.ParameterUpdateMasterContext
import com.example.report.uniplat.executor.ParameterChangeContext
import com.example.report.uniplat.models.event.ModelEventContext
import com.example.report.uniplat.host.Host
import com.example.report.utils.AssertUtils
class model_name {
def model_name = "model_name"
// ====== 以下内容都是可选的,按需添加 ======
// Validator:仅当 action 配置了 "validator": "xxx" 时需要
def my_validator(ActionValidatorContext ctx) { ... }
// Behavior:仅当 action 的 behavior 不是 "insert"/"update"/"delete" 时需要
def my_behavior(ActionBehaviorContext ctx) { ... }
// Updator:仅当 input 配置了 "updator": "xxx" 时需要
def my_updator(ParameterUpdateMasterContext ctx) { ... }
// onChange:仅当 input 配置了 "on_change": "xxx" 时需要
def my_onChange(ParameterChangeContext ctx) { ... }
// 事件处理:仅当 JSON 中配置了 eventSubs 时需要
def my_event_handler(ModelEventContext ctx) { ... }
// 自定义方法:被 invokeModelFunc 调用
def getDisplayInfo(DataObject obj) { ... }
}
| 项目 | 规范 |
|---|---|
| 方法类型 | 实例方法(不是 static),用 def 声明 |
| 返回类型 | 使用 def |
| Validator | 用 AssertUtils 抛异常(也可 throw new ValidateException("...")),不返回错误字符串 |
| Behavior | 返回 BehaviorResult(推荐,见 3.2.1)或字典 [result: 0, id: ..., msg: "..."] |
| Updator | 返回部分覆盖 Map,键须为 meta 字段名(default_value / ext_properties / type / readonly …);动态选项走 ext_properties、隐藏用 "type":"hidden"(无 visible/options 键)。详见 §7.5 |
| package | 可选(建议按分组目录声明,如 package models.system;不声明也能运行) |
| model_name | 类中声明 def model_name = "model_name" |
两种都被平台接受,参考项目里 BehaviorResult 更常用:
import com.example.report.uniplat.executor.BehaviorResult
// 方式 A:BehaviorResult 类(推荐)
def approve_behavior(ActionBehaviorContext ctx) {
def id = ctx.dataList.fetchOne().getKeyValue()
BehaviorResult result = new BehaviorResult(0, "审批成功", id) // (code, msg, id)
result.data = [modelName: model_name] // 可选附加数据
return result
}
// 方式 B:字典(等价,简单场景)
def publish_behavior(ActionBehaviorContext ctx) {
// ...
return [result: 0, id: id, msg: "发布成功"]
}
如果 JSON 中 behavior: "insert"、behavior: "update",Groovy 中不需要写对应方法。
只有以下情况才需要写 Groovy 方法:
validatorbehavior 是自定义方法名updatoron_change属性均可用属性式或方法式访问:
ctx.sender≡ctx.getSender()、ctx.params≡ctx.getParams()、ctx.inputs≡ctx.getInputs()、ctx.host≡ctx.getHost()等。平台按 Groovy 方法参数类型自动匹配对应 Context,写对参数类型即可。
| 类(包) | 用途 | 关键字段/方法 |
|---|---|---|
ActionValidatorContext(uniplat.executor) | 校验 | ctx.dataList、ctx.inputs、ctx.env、ctx.host、ctx.dataModel、ctx.variables |
ActionBehaviorContext(uniplat.executor) | 行为 | ctx.dataList、ctx.inputs、ctx.env、ctx.host、ctx.dataModel、ctx.variables |
ActionCustomInitContext(uniplat.executor) | customInitFunc 动态生成 ActionDef | ctx.dataList、ctx.env、ctx.host |
ParameterUpdateMasterContext(uniplat.executor) | 主表 Updator(字段联动/回显) | ctx.sender、ctx.params、ctx.variables、ctx.env、ctx.dataList、ctx.getParameter();构造 ext_properties 用 buildXxxExtProperties(...)(见 §7.2);setRequired(bool)、getRules() |
ParameterUpdateDetailContext(uniplat.executor) | 详情行 Updator | 同上,附详情行上下文(当前行等) |
ParameterChangeContext(uniplat.executor) | onChange | ctx.params、ctx.host、ctx.variables |
ModelEventContext(uniplat.models.event) | 模型事件 | ctx.getMessage()(.getAction()/.getIds()/.getModelName());取 Host:ctx.getHost() 或 Host.getInstance() 均可 |
ActionPageValuesFuncContext(uniplat.host) | page_values_func 页面值计算 | ctx.masterParams.get("字段")(读列表预过滤参数)、ctx.dataModel、ctx.env |
FilterUpdateContext(uniplat.host) | 列表筛选联动 updator | ctx.sender、ctx.dataModel、ctx.getFilterValues().get("字段") 或 ctx.filterValues.getAt("字段")(读当前筛选值) |
GatewayContext / AnonymousGatewayContext(uniplat.gateway) | 领域服务(标准/匿名) | ctx.host、ctx.body、ctx.parameters、ctx.request、ctx.pathValue(详见 create-service) |
StreamApiContext / AnonymousStreamApiContext(uniplat.gateway) | SSE 流式服务 | 同 GatewayContext + 流式输出 |
ModelServiceContext / AnonymousModelServiceContext(uniplat.models.service) | 模型服务(绑定模型的自定义 API) | ctx.dataModel、ctx.host、请求参数 |
MappingCustomFuncContext(uniplat.models.mapping) | mapping.custom_func 自定义映射 | ctx.dataModel、ctx.env |
JointCustomFuncContext(uniplat.models.joint) | joint.custom_function 自定义关联 | ctx.dataModel、ctx.env |
以下方法名与签名照引擎源码(
models/meta/DataModel.java、engine/DO/DataObject.java、engine/DataList.java)整理,为常用子集;完整 API 以源码为准。
三者关系:DataModel(模型,host.getDataModel("x") 得)→ 查询返回 DataList(列表)→ 取出 DataObject(单条行)。
| 类别 | 方法 | 说明 |
|---|---|---|
| 查询 | queryDataList(Map<String,Object> 过滤) / (Map 过滤, Map<String,Boolean> 排序) / (过滤, 排序, 页, 页大小) | 主查询,返回 DataList。排序值是 Boolean(true=升序、false=降序),不是 "desc" 字符串。过滤支持操作符后缀,见下方说明 |
| 查询 | queryDataList(String sql) / queryDataListWithParams(sql, List params) | SQL 查询 |
| 查询 | queryWithPage(sql, keyField, 页, 页大小) | 分页查询 |
| 查询 | queryCount(Map 过滤) / countByQuerySql(sql) | 计数 |
| 查询 | querySummaryResult(List 汇总字段, Map 过滤) | 聚合(如 ["{amount__sum}"]) |
| 查询 | fetch(字段, List 值) / fetchAll() | 批量/全量取 |
| 取单条 | getByKeyField(id)(int/Long/String 重载) / findByKeyField(id) | 按主键取 DataObject |
| 写 | insertByMap(Map) → BehaviorResult | 按 Map 插入(业务插入,走模型逻辑) |
| 写 | insert(String 表, Map) → long | 直接向表插入,返回自增 id |
| 写 | deleteByMap(Map 条件) / delete(String 表, Map 条件) → int | 按条件删除 |
| 写 | update(String 表, Map 值, Map 条件) | 直接表更新 |
| 写 | upsert(String 表, Map keys, Map values) | 有则更新无则插入 |
| SQL | executeSql(sql) / sqlExecute(sql) / getDataSource() | 执行 SQL / 拿数据源 |
| 日志/备注 | addLog(label, id, userId, Map 参数, invoker) | 写操作日志 |
| 日志/备注 | addRemark(关联id, 备注) → long / queryRemarkList(id, processId) / queryLog(keyValue, 页) | 备注/日志查询 |
| 事件 | sendModelEvent(msg) / sendDelayedModelEvent(msg, 延迟秒) | 发模型事件 |
| 调用 | invokeFunc(方法名, 参数...) / invokeApi/invokeApiAsMap/invokeApiAsList(api名, Map) | 调模型方法/接口 |
| 元信息 | getAction(名) / getFieldDef / getMappingDef / getJointDef / getReferDef / getTable() / getKeyField() / getModelDescription() / getDataModelOfSameSource(模型名) | 元数据 |
| 类别 | 方法 | 说明 |
|---|---|---|
| 取值 | getValue(字段) / getValueOrDefault(字段, 默认) | 取原始值(推荐,字段无则 null) |
| 取值 | get(字段) → PropertyValue(.value / .display) | 取包装对象,.value 值、.display 映射显示 |
| 取值 | getString(字段[,默认]) / getInt(字段[,默认]) / getLong(字段[,默认]) / getBigDecimal(字段) / getDate(字段) / getBoolean(字段) | 类型化取值 |
| 取值 | getDisplay(字段) / getDisplayOrDefault(字段, 默认) | 取显示值 |
| 主键 | getKeyValue() → Long / getKeyFieldValue() → String / getKeyObject() / getKeyDescription() | 主键 |
| 写 | update(Map) / updateWithoutVersion(Map) / delete() | 更新/删除当前行 |
| 关联 | referTo(关联名) → DataObject / referObject(名) / referredBy(模型, 关联[, Map 过滤]) | 引用关系导航 |
| 关联 | getRelationshipDataList(模型[, 谓词]) / addRelationship / removeRelationship | 关系数据 |
| 备注/日志 | addRemark(备注) → long / addLog(label, userId, 参数, invoker) / queryRemarkList() / queryRemarkListWithPage(页, 页大小) | 备注/日志 |
| 流程 | startProcess(...) / getRunningProcess(流程名) / getWorkflow() | 工作流 |
| 其他 | getModel() / sqlQuery(sql) / asPropertyMap() / getVersion() / getFileDisplay/getImageDisplay/getFileUrls | 杂项 |
⚠️ 源码中没有
toMap()/set(k,v)/getDouble()/getTimestamp():转 Map 用asPropertyMap(),写值走update([...])。
| 类别 | 方法 | 说明 |
|---|---|---|
| 取元素 | fetchOne()(空 → null,推荐) / one()(取单条) / get(下标) / singleValue() | 取行 |
| 转换 | asList() → List / asArray() → 数组 / asStream() → Stream / getIdList() | 转集合 |
| 统计 | count() / isEmpty() / calculate(字段, 运算符[, 异常msg]) | 计数/聚合 |
| 关联 | referTo(关联名) / referObject(名) / referredBy(模型, 关联[, Map]) | 为列表批量加载关联 |
| 其他 | getModel() / getList() → DataObject[] / copy() | 杂项 |
⚠️ DataList 本身没有
collect/each/size/list/stream方法:遍历/映射请对asList()结果用 Groovy.each{}/.collect{},流式用asStream()。
综合示例:
def dataModel = ctx.dataModel
def host = ctx.host
// 查询列表 → 取单条
def list = dataModel.queryDataList([status: 1, is_del: 0]) // DataList
def obj = list.fetchOne() // DataObject(空 → null)
def total = list.count()
def ids = list.asList().collect { it.getKeyValue() } // 对 asList() 用 Groovy collect
// 取值
def name = obj?.getValue("name") // 原始值
def status = obj?.getInt("status") // 类型化
def display = obj?.get("status")?.display // 映射显示值
// 写
def newId = dataModel.insertByMap([name: "xxx", status: 1]).id
obj?.update([name: "new_name", status: 2])
obj?.delete()
// 关联 / joint
def refName = obj?.referTo("owner")?.getValue("name") // reference 导航
def jointVal = obj?.get("id#member_role_joint.role_names")?.value // joint(# 语法)
queryDataList(Map 过滤) 的过滤键默认按 字段 = 值 匹配。要用其他操作符,在键名后加 __操作符 后缀(双下划线):
dataModel.queryDataList([
"status__ne" : 0, // status != 0
"age__gte" : 18, // age >= 18
"type__in" : [1, 2, 3], // type IN (1,2,3);值为 List 会自动按 in 处理
"name__matchFuzzy": "张", // 模糊匹配
"deleted_time__isnull": 1, // IS NULL
"id" : 100 // 无后缀 = 相等
], ["id": false]) // 排序:id 降序(Boolean)
常用操作符(源码 models/field/sql/SqlClauseBuilderOperators):=(默认)、ne(≠)、in、nin(not in)、gt/gte/lt/lte、matchStart(前缀)、matchFuzzy(模糊)、fullText(全文)、isnull、notnull。
值为
List时引擎自动转为__in(models/meta/DataModel.java)。排序 Map 的值是 Boolean(true=升序、false=降序)。
Groovy 钩子都由 SubProject 按方法参数类型分发。除常见的 Validator/Behavior/Updator/onChange 外,还有:
| 钩子 | JSON 配置处 | 方法参数类型 | 用途 |
|---|---|---|---|
| Validator | action.validator | ActionValidatorContext | 执行前校验 |
| Behavior | action.behavior(自定义名) | ActionBehaviorContext | 核心业务逻辑 |
| Updator | input.updator | ParameterUpdateMasterContext(详情行用 ...DetailContext) | 字段联动 |
| onChange | input.on_change | ParameterChangeContext | 字段变更回调 |
| page_values_func | action.page_values_func | ActionPageValuesFuncContext | property2 时计算页面初值(可读 masterParams) |
| customInitFunc | action.customInitFunc | ActionCustomInitContext | 动态生成/改写 ActionDef |
| 筛选联动 updator | list 筛选项的 updator | FilterUpdateContext | 列表筛选项级联(读 filterValues) |
| mapping 自定义 | mapping_defs[].custom_func | MappingCustomFuncContext | 动态生成映射值 |
| joint 自定义 | joint_defs[].custom_function | JointCustomFuncContext | 用函数替代 joint SQL |
| 批量 Validator/Behavior | 批量 action | 批量上下文 | 批量操作校验/执行 |
| 模型事件 handler | eventSubs[].handler | ModelEventContext | 数据变更事件 |
| DataInterface | 数据接口 | — | 对外数据接口 |
| ModelService | /model/{模型}/service/{func} | ModelServiceContext | 绑定模型的自定义 API(见 create-service) |
条件表达式和 server 参数里用 env.*,Groovy 里可拿 EnvDataObject:
def env = ctx.env // 或 EnvDataObject.getUserEnv()
def userId = env.getUserId() // 当前用户 ID
def xid = env.getXid() // 当前组织 ID
内置环境变量(JSON 表达式/server 参数常用):user_id、xid、org_id、date、month、now。例:
{"property": "created_by", "result": "env.user_id.value"}
{"property": "created_time", "result": "env.now.value"}
一次 Action 执行的引擎顺序(理解 when/enable 与 Variables 的时机):
条件检查: enabled()(是否可用)/ whenPassed()(是否显示)/ authPassed()(权限)
→ 准备: getVariables() / getPageValues()(page_values_func)
→ 校验: doValidate() → Validator
→ 执行: doBehavior() → Behavior
→ 后处理: fireEvent() / executeCalls()
Action Variables:action 级状态变量,property2 阶段算初值、经 onChange 更新,execute 时钩子里 ctx.variables 可读。JSON 声明:
{
"name": "create_order",
"variables": [
{"name": "discount", "defaultValue": 1.0},
{"name": "total_amount", "defaultValue": 0}
]
}
在 Groovy 中处理模型数据变更事件:
import com.example.report.uniplat.models.event.ModelEventContext
class model_name {
def model_name = "model_name"
def onModelChange(ModelEventContext ctx) {
def msg = ctx.getMessage()
def action = msg.getAction() // "insert", "update", "delete", 或自定义
def ids = msg.getIds() // 受影响的数据 ID 列表
def modelName = msg.getModelName()
// 取 Host:ctx.getHost() 与 Host.getInstance() 均可用
def host = ctx.getHost() // 或:def host = Host.getInstance()
if (action == "insert") {
def model = host.getDataModel("model_name")
def obj = model.getByKeyField(ids[0])
// 处理新增事件...
}
}
}
JSON 中配置事件订阅:
"eventSubs": [
{
"action": "insert",
"handler": "onModelChange"
}
]
def host = Host.getInstance()
host.sendModelEvent("model_name", "custom_action", dataObject)
package services
import com.example.report.uniplat.gateway.GatewayContext
import com.example.report.uniplat.host.Host
class service_name_api {
def methodName(GatewayContext ctx) {
def host = Host.getInstance()
def body = ctx.getBody()
def param = body.get("param")?.toString()
def model = host.getDataModel("system_user")
def list = model.queryDataList([status: 1])
return list.list.collect { [id: it.getKeyValue(), name: it.name.value] }
}
}
API 端点(领域服务,标准/匿名两种最常用;完整 7 种品类 + 模型服务见 create-service):
POST /general/project/{subproject}/service/{ServiceClass}/{method} # 标准,需认证
POST /general/project/{subproject}/service/anonymous/{ServiceClass}/{method} # 匿名,免登录
另有:内部服务
internal_service/、SOA/soa/{project}/{name}/{func}、SSE 流式stream/、MVCctrl/;以及模型服务/general/model/{模型}/service/{func}(ModelServiceContext)。
GatewayContext 字段:
ctx.getBody().get("key") / ctx.body.key — body 参数(两种皆可)ctx.getStringParam("key") / ctx.getIntParam("key", 默认) / ctx.getLongParam("key") — URL 参数(类型化)ctx.parameters["key"]?.getAt(0) — URL 参数(原始,值是数组)ctx.getRequest() / ctx.request — HttpServletRequestJSON:
{
"name": "delete",
"when": "object != null && object.getValue('is_del') == 0",
"parameters": {
"server": [
{"property": "is_del", "result": "1"},
{"property": "updated_time", "result": "env.now.value"}
],
"inputs": []
},
"behavior": "update",
"forward": "refresh"
}
list 中加过滤:
"dataFilters": [{"field": "is_del", "value": "0"}]
DML(insert/update/delete)必须在事务中执行;事务不能嵌套。 用 DataSourceFactory.transaction(数据源, 闭包),闭包体即事务体(可返回值):
import com.example.report.db.DataSourceFactory
// 数据源:用模型的 getDatabase()(推荐,自动对齐模型所在库),或直接写库名字符串
def model = host.getDataModel("xxx")
DataSourceFactory.transaction(model.getDatabase(), {
obj1.update([...])
obj2.delete()
})
DataSourceFactory.transaction("uniplat_task", {
// ... DML ...
})
⚠️ 不要嵌套事务:
transaction(...)内部再调用一个开事务的方法会出问题。若一段逻辑可能被已在事务中的调用方复用,用带runInCurrentTransaction参数的重载复用当前事务,而非再开一个。
import com.example.report.db.DataSourceFactory
import com.example.report.db.DbConsts
def ds = DataSourceFactory.getDataSource(DbConsts.HOST)
def results = ds.queryForList("SELECT * FROM table WHERE status = ?", 1)
updator 返回 Map 是部分覆盖,键须为 InputParameterMeta 字段名,未返回的键保留 JSON 静态配置。动态选项走 ext_properties(用 §7.2 的 build 方法)、隐藏用 "type":"hidden"、必填用 ctx.setRequired(true)。
def my_updator(ParameterUpdateMasterContext context) {
if (context.sender == "" || context.sender == "type_id") {
return [
"default_value" : "1",
"readonly" : false,
// 动态选项:用 build 方法产出 ext_properties,而不是手拼 options
"ext_properties": context.buildMappingExtProperties("mapping", "type_mapping")
]
}
return [:]
}
⚠️ 没有
visible/options返回键(写了会抛NoSuchFieldException)。返回键的合法集合、两个默认值字段、各类型默认值格式、单/多 intentSearch 与 cascader 的坑,见 §7「表单字段渲染与编辑回显」。
⚠️ 「读当前列表的过滤值」不在
ActionBehaviorContext,取值方式取决于所在的 Context 类型:
A. 页面值计算函数 page_values_func —— ActionPageValuesFuncContext(uniplat.host)
用 ctx.masterParams.get("字段") 读列表的预过滤参数(masterParams):
import com.example.report.uniplat.host.ActionPageValuesFuncContext
def emplyeeInAgentNumber_func(ActionPageValuesFuncContext ctx) {
def emplyeeInId = ctx.masterParams.get("EmplyeeInId") // 读预过滤参数
return ctx.dataModel.getDataSource().queryForList(
"select ... where ea.EmplyeeInId=?", emplyeeInId)
}
B. 列表筛选联动 updator —— FilterUpdateContext(uniplat.host)
用 ctx.getFilterValues().get("字段") 或 ctx.filterValues.getAt("字段") 读当前筛选值:
import com.example.report.uniplat.host.FilterUpdateContext
def group_updator(FilterUpdateContext ctx) {
def sender = ctx.sender
def groupType = ctx.getFilterValues().get("GroupType") // 读当前筛选值
def list = ctx.dataModel.getDataSource().queryForList("... where type=?", groupType)
return [ext_properties: [mapping_values: list]]
}
环境信息(各 Action Context 通用):
def xid = ctx.env.xid.value // 当前组织
def userId = ctx.env.user_id.value // 当前用户
手机号、身份证、银行卡等敏感字段用现成配置脱敏,不要自己写正则。在 field_defs[] 的字段上加 desensitizeType:
{"property": "mobile", "label": "手机号", "type": "text", "desensitizeType": "mobile"}
desensitizeType 枚举(config/schemas/datamodel.json):name、mobile、mobile_list、bankCard、email、idCardNum、password。特殊脱敏规则可用 desensitizeFunc 指定自定义方法名。
本章讲 Action 的
parameters.inputs[](弹窗表单项)怎么渲染、联动、编辑回显——每天在写、却最易踩坑的一层。API 断言可追溯到引擎源码executor/InputParameter.java、executor/InputParameterMeta.java、executor/ParameterUpdateMasterContext.java。
引擎按 input 的 type 分发到不同的 ext_properties 构造分支(executor/InputParameter.java:180-212 的 switch 是权威依据)。下表覆盖项目高频类型,四列:用途 / default_value 格式 / updator 里如何构造 ext_properties / 编辑回显写法。
| type | 用途 | default_value 格式 | ext_properties 构造 | 编辑回显 |
|---|---|---|---|---|
| text / textarea | 单/多行文本 | 标量串 | 无(base) | 静态 default_value 表达式 |
| number / money | 数字/金额 | 标量 | 无 | 静态表达式;.toString() 安全 |
| boolean | 开关 | true/false | 无 | 静态表达式 |
| hidden | 隐藏域 | 标量 | 无 | 静态表达式;updator 可 "type":"hidden" 动态隐藏 |
| tip | 纯提示文本 | — | 无 | — |
| mapping | 单选映射 | 选项 key(标量) | buildMappingExtProperties("mapping", 映射名) | 静态表达式给 key |
| multi_mapping | 多选映射 | key 列表 | buildMappingExtProperties("multi_mapping", 映射名) | updator 回显 |
| checkbox-group / radio-group | 多选框/单选组 | 同 mapping | buildMappingExtProperties(type, 映射名) | — |
| cascader | 级联选择 | ⚠️ 层级数组 [一级key, 二级key] | buildMappingExtProperties("cascader", 映射名)(首参必须 "cascader",见 §7.4) | 静态 default_value 给层级数组 |
| mapping_tree | 树形映射 | key | buildMappingExtProperties("mapping_tree", 映射名) | — |
| tree / tree_cascader | 树选择 | 节点 key | 引擎按 treePrefilters 自动构造(无专用 build 方法) | 静态表达式 |
| intentSearch | 单选实体搜索 | 原始 key 值(不 JSON 化) | buildIntentSearchExtProperties(intentName, "{模板}", 值, false) | 见 §7.3 |
| multi_intentSearch | 多选实体搜索 | ⚠️ JSON 数组字符串 ["1","2"](JsonUtils.dumps(list)),空传 "" | buildIntentSearchExtProperties(intentName, "{模板}", JSON串, true) | 见 §7.3 |
| employee_select | 员工选择 | key/JSON(按控件) | buildEmployeeSelectExtProperties()(无参) | updator 回显 |
| image / multi_image / file / multi_file | 图片/文件 | URL 串 / JSON | 引擎按 uploadConfig 自动构造 | 静态表达式 |
| rich_text | 富文本 | HTML 串 | 引擎自动构造 | 静态表达式 |
未列出的冷门控件(superCascader / app_member_select / org_manager_select / joint search / multi_search 等)同样在上述 switch 里各有分支,用法参照对应
buildXxxExtProperties。控件类型全集见config/schemas/datamodel.json的T_EDITOR_TYPE。
buildXxxExtProperties 系列字段联动/回显要动态换选项、换映射、换搜索意图时,不要手拼 ext_properties 的 Map,用 ParameterUpdateMasterContext(详情行用 ParameterUpdateDetailContext)上的现成方法(executor/ParameterUpdateMasterContext.java):
| 方法 | 适用 type | 关键参数 |
|---|---|---|
buildMappingExtProperties(type, 映射名) | mapping / multi_mapping / cascader / mapping_tree / checkbox-group / radio-group | 首参 = 字段类型字符串(executor/InputParameter.java:222) |
buildIntentSearchExtProperties(intentName, 文本模板, 默认值, isMulti[, multipleLine]) | intentSearch / multi_intentSearch | 末参 isMulti 决定默认值格式(见 §7.3);默认值为 String |
buildJointExtProperties(jointName, 文本模板, 默认值, isMulti) | search / multi_search(joint) | 默认值为 String |
buildEmployeeSelectExtProperties() | employee_select | 无参 |
buildSuperCascaderExtProperties(名, 值) / buildAppMemberSelectExtProperties() / buildOrgManagerSelectExtProperties() | 对应控件 | — |
另有 ctx.setRequired(boolean)(动态改必填)、ctx.getRules()(读校验规则)。
intentSearch(单选)与 multi_intentSearch(多选)默认值格式不一样,写错直接运行时崩溃:
multi_intentSearch(isMulti=true):default_value 必须是 JSON 数组字符串,如 ["78122","139427"];用 JsonUtils.dumps(list) 构造,空值传 ""。引擎会 JsonUtils.fromJson(值, ArrayList.class) 解析它(executor/InputParameter.java:357)——传逗号串 "78122,139427" 会抛 JsonParseException。intentSearch(isMulti=false):传原始 key 值(如 "78122"),引擎走 getByKeyField(值)(或 intent 配了 selectedField 时按该字段查,executor/InputParameter.java:340-351)——不要 JSON 化。import com.example.report.utils.JsonUtils
// 多选回显:已选员工 eid 列表 → JSON 数组串
def contactor_updator(ParameterUpdateMasterContext context) {
if (context.sender == "") { // 初始化/回显阶段,见 §7.6
def object = context.dataList.fetchOne()
def dv = ""
if (object != null) {
def eids = context.host.getDataModel("partner_product_contactor")
.queryDataList(["product_id": object.getKeyValue()]).asList()
.collect { it.getString("eid") }
if (!eids.isEmpty()) dv = JsonUtils.dumps(eids) // ✅ JSON 数组串,不是逗号串
}
return [
"default_value" : dv,
"ext_properties": context.buildIntentSearchExtProperties("system_emp_intent", "{name}", dv, true)
]
}
return [:]
}
证据:正确用例
hro/hro_spview/models/partner/partner_product.groovycontactor_updator。
pid 的懒加载映射)的 updator 必须 buildMappingExtProperties("cascader", 映射名)——首参传 "cascader"。否则触发断言 懒加载的mapping只支持级联cascader使用(executor/InputParameter.java:229-230:非 cascader 类型时禁止 ModelLazyMapping)。default_value 是层级数组 [一级key, 二级key],不是标量。回显优先用静态 default_value 表达式给层级数组,updator 只负责按条件切换映射树。def category_type_updator(ParameterUpdateMasterContext context) {
if (context.sender == "") {
def object = context.dataList.fetchOne()
if (object == null) return [:]
def user_type = object.getInt("user_type")
def mapping_name = user_type == 1 ? "category_type_qf_mapping"
: user_type == 2 ? "category_type_xb_mapping"
: "category_type_mapping"
return [
"type" : "cascader",
"ext_properties": context.buildMappingExtProperties("cascader", mapping_name) // ✅ 首参 "cascader"
]
}
return [:]
}
证据:
hro/hro_spview/models/partner/partner_product.groovycategory_type_updator(分类回显由静态 default_value 层级数组提供,updator 只切映射树)。
部分覆盖,不是整体替换。 引擎只把 updator 返回 Map 里出现的键反射写入 meta,未返回的键保留 JSON 静态配置(executor/InputParameterMeta.java:91-100 遍历返回键做反射 set;executor/InputParameter.java:110 meta.update(updatorResult))。
返回键必须是 InputParameterMeta 的字段名,否则反射抛 NoSuchFieldException(InputParameterMeta.java:102-111)。常用合法键:
| 键 | 作用 |
|---|---|
default_value | 覆盖已求值的默认值(回显值) |
ext_properties | 覆盖控件扩展属性(用 §7.2 的 build 方法产出) |
type | 改控件类型;隐藏字段用 "type":"hidden"(没有 visible 键) |
readonly / clearable / filterable / placeholder / label / multi | 对应 meta 字段 |
input(特殊) | 嵌套 Map,改 InputParameterDef 字段(如 required);引擎单独处理(InputParameter.java:50-52) |
⚠️ 没有
visible、options这两个键:动态选项走ext_properties(返回[ext_properties: [mapping_values: [...]]]会被引擎自动包装成 mapping,InputParameter.java:59-67);动态必填用ctx.setRequired(true)。旧写法return [visible:.., options:..]是错的,会崩。
两个默认值字段(InputParameterMeta.java:30-31):defaultValueExp(原始表达式串)与 default_value(引擎已求值的 Object,前端实际用它回显)。updator 返回的 "default_value" 键覆盖后者。
推论范式:能用静态 default_value 表达式回显就优先用它;updator 只负责"改结构/切映射/联动显隐"。若 updator 必须返回默认值,注意类型——cascader 给数组、multi_intentSearch 给 JSON 串,不要无脑 .toString()(标量字段如金额才可 .toString())。
updator 由 ctx.getSender() 区分触发时机:
sender == "":表单初始化 / 回显阶段(property2)。编辑弹窗的回显逻辑写在这里——用 ctx.dataList.fetchOne() 取当前行、按各控件格式算 default_value。非 is_param 字段的 updator 同样会在此触发。sender == "<字段名>":名为该字段的输入项刚变更,触发联动——用 ctx.params.get("<字段名>") 读它的新值。ctx.getParameter().getProperty() 拿"当前 updator 挂在哪个字段"。
def product_id_updator(ParameterUpdateMasterContext context) {
def sender = context.sender
def property = context.parameter.property // 当前字段
if (sender == "") {
// —— 回显骨架:从已有行取值填回 ——
def obj = context.dataList.fetchOne()
return obj == null ? [:] : ["default_value": obj.getString(property)]
} else if (sender == "order_type") {
// —— 联动:order_type 变了,按新值调整本字段 ——
def order_type = context.params.get("order_type") as String
return order_type == "专项业务" ? ["type": "intentSearch"] : ["type": "hidden"]
}
return [:]
}
证据:
hro/hro_spview/models/partner/partner_product.groovyorder_type_updator。
把上面串起来,一套"编辑对齐新增"的落地顺序:
default_value 表达式(如 "object.getValue('name')";cascader 写层级数组表达式)。引擎在 property2 阶段自动求值回显(InputParameter.java:74)。sender == "" 分支回显(§7.6)。"type":"hidden",必填用 setRequired。type / default_value 格式 / ext_properties(mapping/intent 名)/ required / updator 分型逻辑。when(显示)与 enable(启用)分清:引用 object 时先 object != null && 再判业务条件is_del = 0def 声明throw new ValidateException)BehaviorResult(推荐)或字典 [result: 0, id:, msg:]obj.getValue("字段");obj.字段.value 直取,可空字段加 ?.import 再用短名,如 import java.text.SimpleDateFormat 后 new SimpleDateFormat(...),不要写 new java.text.SimpleDateFormat(...)DataSourceFactory.transaction(model.getDatabase(), {...}) 包裹(见 §6.2)buildXxxExtProperties、隐藏用 "type":"hidden"(无 visible/options 键)| 内容 | 位置 |
|---|---|
| JSON Schema | uniplat-main/config/schemas/datamodel.json |
| 引擎类源码(方法权威来源) | uniplat-main/src/main/java/.../models/meta/DataModel.java、.../engine/DO/DataObject.java、.../engine/DataList.java |
| Context 类源码 | uniplat-main/src/main/java/.../executor/(Action*/Parameter* 上下文)、.../host/(PageValuesFunc/FilterUpdate 上下文) |
| 表单渲染/回显权威源码 | .../executor/InputParameter.java(type→ext_properties 分发、默认值求值)、.../executor/InputParameterMeta.java(updator 合并语义、两个默认值字段)、.../executor/ParameterUpdateMasterContext.java(buildXxxExtProperties) |
| 基础模型 | uniplat_base/models/ |
| 公共模型 | uniplat_common/models/ |
| 业务模型 | biz/{子项目}/models/ |
| 领域服务 | biz/{子项目}/services/ |
| 常量定义 | {子项目}/consts/ |
config/schemas/datamodel.json 了解 JSON Schemawhen/enable 引用 object 时已 object != null && 守卫