# 对象动作(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
1

# 模式与显示位置

模式 下拉框显示 含义 在列表中的位置
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
1

注意

当前版本的获取动作列表接口不会返回 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 }
]
1
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()}"]
1
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"
]
1
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 条"]
)
1
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 }
}
1
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": { }
}
1
2
3
4
5
6

formId 为触发动作的表单 id,平台据此确定对象类型;formValues 为参数表单的取值。同步执行时,接口返回本次执行的状态(status)、execResult、execLog、download、stackTrace 等;异步执行时立即返回,结果完成后推送给用户。

Last Updated: 2026/9/24 14:27:35