# 对象动作(Dynamic Action)
对象动作用于在列表页面上增加自定义操作按钮,例如"锁定用户""激活主题""批量加入用户组"。点击按钮后,平台执行动作的核心逻辑,并把执行结果展示给用户。
# 目标读者
本文档的目标读者为:本系统的开发和实施人员
# 相关菜单
| 菜单 | 用途 |
|---|---|
开发定制 > 对象动作 > 动作定义 | 维护动作本身(DynamicAction) |
开发定制 > 对象动作 > DynamicActionDynamicForm | 把动作挂到表单上,决定动作出现在哪个列表中 |
开发定制 > 对象动作 > 动作组 | 维护动作分组(DynamicActionGroup),当前版本分组不生效,见 挂载到表单 |
执行记录 > 对象动作 | 查看每次执行的记录(DynamicActionExecRecord) |
# 动作定义
在 开发定制 > 对象动作 > 动作定义 点击"创建",或通过 DynamicAction.csv 种子数据导入动作定义。

| 字段 | 说明 |
|---|---|
| 名称(name) | 必填,唯一,创建后不可修改 |
| 显示名称(label) | 必填,按钮上显示的文字 |
| 帮助信息(helpText) | 动作的说明;动作带参数表单时显示在表单上方 |
| 图标(icon) | 按钮图标,参见 图标说明 |
| 扩展信息(extInfo) | JSON,控制显示和执行行为,见 可用 extInfo 列表 |
| 启用逻辑(enableLogic) | 类型为 DYNAMIC_ACTION_ENABLE_LOGIC 的动态逻辑,决定动作对哪些记录可用,见 启用逻辑 |
| 核心逻辑(coreLogic) | 类型为 DYNAMIC_ACTION_LOGIC 的动态逻辑,动作实际执行的代码,见 核心逻辑。可以为空,为空时参数表单上没有提交按钮 |
| 模式(mode) | OBJECT_SINGLE / OBJECT_MULTIPLE / CLASS_LEVEL,见 模式与显示位置 |
| Form(form) | 参数表单,类型为 ACTION 的表单,见 参数表单 |
| 启用异步处理(enableAsync) | 为 true 时在后台执行,前台立即返回,执行完成后推送结果消息 |
| 显示提示信息(confirmMessage) | 不为空时,执行前弹出确认框显示该文字 |
| 是否生效中(active) | 只在编辑表单中显示。为 false 时按钮仍会显示,但执行时返回 Action not found |
| 访问权限(accessRequirement) | 执行该动作需要满足的角色要求(RoleRequirement),取值如 USER、DEVELOPER、ADMIN |
必须设置访问权限
accessRequirement 为空的动作不会出现在任何列表中,直接调用执行接口也会返回 No permission to execute action。当前版本的创建和编辑表单中都没有这个字段,在界面上创建动作后,需要通过 CSV 导入或数据 API(PUT /data/DynamicAction/{id},请求体 {"accessRequirement": {"id": <RoleRequirement id>}})补上。
创建与编辑表单的分组标题(Basic Information、Logics、Execution Attributes)、"Form" 字段以及模式下拉框中的选项(Single object、Multiple objects、Class level action)目前显示为英文;弹窗标题显示为"对像动作",是当前版本界面上的错字,应为"对象动作"。
CSV 种子数据的表头如下:
name(*),mode,form.name,confirmMessage,coreLogic.name,enableLogic.name,label,icon,helpText,enableAsync,isSystem,extInfo,active,accessRequirement.name
# 模式与显示位置
| 模式 | 下拉框显示 | 含义 | 在列表中的位置 |
|---|---|---|---|
OBJECT_SINGLE | Single object | 作用于一条记录 | 每行"操作"列末尾的下拉菜单 |
OBJECT_MULTIPLE | Multiple objects | 作用于选中的一条或多条记录 | 表格上方的工具栏,勾选记录后可用 |
CLASS_LEVEL | Class level action | 不针对具体记录,作用于整个对象类型 | 表格上方的工具栏,始终可用 |
下图中,「折扣价试算」是 CLASS_LEVEL 动作,显示在工具栏;「查看账号状态」是 OBJECT_SINGLE 动作,显示在行末的下拉菜单中:

用户列表中的"User Groups"列名目前没有翻译,是当前版本的界面问题。
# 参数表单
动作需要用户输入参数时,把一个类型为 ACTION 的表单设置到 Form 字段。点击动作后,平台在抽屉(默认)或弹窗中显示该表单,用户填写后点击提交执行:
- 用户填写的值在核心逻辑中以
parameters变量提供。平台会先按表单字段的类型转换取值,转换前的原始值为rawParameters。 - 抽屉还是弹窗、提交按钮文字、结果展示方式等,可以在参数表单的
extInfo.action中设置,也可以写在动作的extInfo中,键名见 可用 extInfo 列表。两处不会合并:只要参数表单的extInfo中有action键,动作自己的extInfo就整体不再生效。 - 参数表单模式下,执行完成后结果默认显示在抽屉/弹窗中,提交按钮变为重新执行按钮。
没有参数表单的动作点击后直接执行;confirmMessage 不为空时先弹出确认框。
# 挂载到表单
动作本身不与领域模型直接关联,而是通过 DynamicActionDynamicForm 挂到表单上:动作挂在哪个列表表单上,就出现在哪个列表页面中。在 开发定制 > 对象动作 > DynamicActionDynamicForm 中维护,字段如下:
| 字段 | 说明 |
|---|---|
action | 必填,动作 |
form | 必填,表单,通常是某个对象的列表表单 |
displaySequence | 必填,显示顺序,值越小越靠前 |
group | 动作组(DynamicActionGroup) |
同一个动作在同一个表单上只能挂一次。CSV 种子数据的表头如下:
action.name(*),form.name(*),displaySequence,group.name
注意
当前版本的获取动作列表接口不会返回 group 信息,前端也不会按分组合并显示,设置 group 暂时没有效果。
# 启用逻辑
平台在返回动作列表时,对当前选中(或当前行)的记录执行动作的启用逻辑,决定动作对哪些记录可用。启用逻辑是类型为 DYNAMIC_ACTION_ENABLE_LOGIC 的动态逻辑。
执行动作前,平台会再执行一次启用逻辑,所选记录中只要有一条不可用,本次执行即失败,执行记录的结果为 Action xxx is disabled for ...。
# 注入变量
| 变量名称 | 变量类型 | 描述 |
|---|---|---|
userContext | tech.muyan.api.security.MuyanAuthentication | 当前操作的用户信息 |
action | tech.muyan.dynamic.action.DynamicAction | 当前动作定义 |
form | tech.muyan.dynamic.form.DynamicForm | 动作所在的表单 |
objects | List | 待判断的记录列表 |
domainClass | tech.muyan.DomainClass | 记录的对象类型信息 |
log | Closure | 打印执行日志 |
application | grails.core.GrailsApplication | 当前的 grails 应用上下文 |
# 返回结果
启用逻辑返回可用记录的 id 列表:
// 只有已锁定的用户可以执行"激活用户"
return [
enableIds: objects.findAll { it.accountLocked }.collect { it.id }
]
2
3
4
- 返回的 Map 中没有
enableIds(或其值为null)时,视为全部记录可用。 - 启用逻辑抛出异常时,视为全部记录不可用。
- 没有配置启用逻辑时,全部记录可用。
启用逻辑对 CLASS_LEVEL 动作无效
列表中没有选中任何记录时,平台不执行启用逻辑;CLASS_LEVEL 动作执行时也没有待判断的记录,因此启用逻辑不能禁用 CLASS_LEVEL 动作。需要限制谁能执行时,请使用 accessRequirement。
# 核心逻辑
核心逻辑是类型为 DYNAMIC_ACTION_LOGIC 的动态逻辑,也就是动作实际执行的代码。
# 注入变量
| 变量名称 | 变量类型 | 描述 |
|---|---|---|
userContext | tech.muyan.api.security.MuyanAuthentication | 当前操作的用户信息 |
action | tech.muyan.dynamic.action.DynamicAction | 当前动作定义 |
form | tech.muyan.dynamic.form.DynamicForm | 触发动作的表单 |
objects | List | 选中的记录列表。OBJECT_SINGLE 模式下也是列表,只有一个元素;CLASS_LEVEL 模式下为空列表 |
objectIds | List<Long> | 选中记录的 id 列表 |
domainClass / objectType | tech.muyan.DomainClass | 记录的对象类型信息,两个变量是同一个值 |
parameters | Map<String, Object> | 参数表单中用户填写的值,已按字段类型转换 |
rawParameters | Map<String, Object> | 参数表单中用户填写的原始值 |
searchConditions | Map | 执行时列表页面当前的搜索条件,见 按搜索条件批量处理 |
log | Closure | 打印执行日志,内容保存到执行记录的"执行日志" |
application | grails.core.GrailsApplication | 当前的 grails 应用上下文 |
示例:查看选中用户的账号状态(OBJECT_SINGLE 动作)
def user = objects[0]
String status = user.accountLocked ? "已锁定" : "正常"
log("查看账号状态:${user.username}")
return [execResult: "用户:${user.name}(${user.username})\n账号状态:${status}\n创建时间:${user.dateCreated.toLocalDate()}"]
2
3
4
# 按搜索条件批量处理
在列表页面执行动作时,前端会把搜索面板当前的条件作为 searchConditions 一起提交。需要对"所有符合当前搜索条件的记录"做批量处理(而不只是当前页勾选的记录)时,可以在核心逻辑中根据 searchConditions 自行查询。
# 执行结果
# 返回值约定
核心逻辑正常执行时,返回如下结构的 Map:
[
// 执行结果文字,保存到执行记录并显示给用户
// Execution result text, saved to the execution record and shown to the user
execResult: "执行的结果/Execution result",
// 类型为 tech.muyan.storage.StorageFieldValue,前端执行完成后自动下载该文件
// Type is tech.muyan.storage.StorageFieldValue, the frontend downloads it automatically
download: storageFieldValue,
// 以下字段只保存到执行记录,当前前端不使用
// Following fields are only saved to the execution record, not used by the current frontend
displayType: "markdown",
execResultInfo: "补充信息/Extra info",
redirect: "/some/page"
] 2
3
4
5
6
7
8
9
10
11
12
execResult:执行结果文字,保存到执行记录,并显示给用户。download:值为tech.muyan.storage.StorageFieldValue时,前端执行完成后自动下载该文件。其他类型的值会被忽略。displayType、execResultInfo:会保存到执行记录中,但当前版本的执行接口不返回这两个字段,前端展示结果时不会使用它们。redirect:会保存到执行记录,但当前前端不处理跳转。
# 异常与执行状态
| 情况 | 执行状态 |
|---|---|
| 正常返回 | SUCCESS |
抛出 tech.muyan.exception.CustomLogicWarningException(或其子类) | SUCCESS_WITH_WARNING,异常消息追加到执行日志 |
| 抛出其他异常 | FAILED,异常消息作为执行结果,堆栈保存到"错误堆栈" |
| 启用逻辑判定所选记录不可用 | FAILED |
抛出警告异常时仍希望返回结果,可以使用构造函数 CustomLogicWarningException(String message, Object data),其中 data 为 Map,平台读取其中的 execResult 和 download:
throw new tech.muyan.exception.CustomLogicWarningException(
"部分记录已跳过",
[execResult: "成功处理 8 条,跳过 2 条"]
)
2
3
4
执行记录还有 NOT_START(已提交未开始)和 RUNNING(执行中)两种中间状态。
# 结果展示
- 没有参数表单的动作,结果默认以消息提示(toast)显示;在动作的
extInfo中设置"resultType": "inContainer"后,结果显示在弹窗中。弹窗中的执行结果不保留换行。 - 有参数表单的动作,结果默认显示在参数表单所在的抽屉/弹窗中;设置
"resultType": "toast"后改为消息提示并关闭抽屉/弹窗。
下图是「查看账号状态」动作(resultType 为 inContainer)使用上面的示例代码执行的结果。换行符不生效,三行内容连成一段显示(过长时按弹窗宽度自动折行):

# 执行记录
每次执行都会在 DynamicActionExecRecord 中保存一条记录,在 执行记录 > 对象动作 中查看,包括执行人、对象、状态、执行结果、执行日志、错误堆栈、下载文件等。
注意
执行记录当前无法通过界面或数据 API 删除,而动作被执行记录引用后也就无法删除。需要下线动作时,请删除它在 DynamicActionDynamicForm 中的挂载记录,并关闭"是否生效中"。
# 可用 extInfo 列表
如下是动作可用的 extInfo 键,写在动作的"扩展信息"中,或写在参数表单 extInfo 的 action 键下:
{
/** 参数表单的显示方式:drawer 抽屉(默认)或 modal 弹窗 */
/** How the parameter form is shown: drawer (default) or modal */
"layout"?: "drawer" | "modal",
/** 参数表单提交按钮的文字,默认为"提交" */
/** Text of the submit button of the parameter form */
"submitButtonText"?: string,
/** 结果展示方式:toast 消息提示,inContainer 显示在弹窗/抽屉中。
没有参数表单时默认 toast,有参数表单时默认 inContainer */
/** How the result is shown: toast, or inContainer (inside the modal/drawer).
Defaults to toast without a parameter form, inContainer with one */
"resultType"?: "toast" | "inContainer",
/** 弹窗样式,当前只使用 width(layout 为 modal 时生效) */
/** Modal style, only width is used (when layout is modal) */
"style"?: { "width"?: number | string }
} 2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 相关接口
下表路径省略了 /api 前缀,从外部调用时需加上,见 地址前缀。
| 接口 | 说明 |
|---|---|
GET /action/byFormId/{formId}?ids=1,2 | 获取挂在该表单上、当前用户有权限的动作列表;ids 为待判断的记录,返回的 enableIds 为其中可用的记录 |
POST /action/byId/{actionId} | 按 id 执行动作 |
POST /action/byName/{actionName} | 按名称执行动作 |
执行接口的请求体:
{
"ids": [1, 2],
"formId": 38,
"formValues": { "remark": "..." },
"searchConditions": { }
}
2
3
4
5
6
formId 为触发动作的表单 id,平台据此确定对象类型;formValues 为参数表单的取值。同步执行时,接口返回本次执行的状态(status)、execResult、execLog、download、stackTrace 等;异步执行时立即返回,结果完成后推送给用户。