# 数据导入

# 目标读者

本文档的目标读者可能包括:

  • 本系统的开发及实施人员
  • 准备导入数据的用户数据管理员

# 功能说明

本系统遵循一切皆数据的原则:领域模型、表单、菜单、权限、动态逻辑等元数据和业务数据一样,都可以通过 CSV 文件导入,导入时支持新建、更新、删除。

提示

本系统的 CSV 解析格式基于 RFC 4180 (opens new window) 标准,另外把反斜杠(\)作为转义符,见特殊字符的转义。

种子数据(Seed Data)一般是指在应用程序初始化或部署时插入数据库的一组初始数据。牧言低代码平台的设计原则是包括定制代码在内的一切皆数据,整个业务系统完全可以由种子数据构建。

平台当前没有数据导出功能,界面和接口都不提供把数据导出为 CSV 的能力。

如果你习惯通过例子学习,可以直接查看一个完整的例子。各类平台对象的 CSV 列说明见 CSV 导入模板。

# 导入方式

当前支持三种导入方式:

方式 适用场景
系统启动时自动导入 开发环境、插件开发,随代码一起维护的种子数据
插件数据导入 以插件包的形式发布、安装一组数据
在列表页导入数据 在界面上向某个模型批量导入数据

# 系统启动时自动导入

后端每次启动时,按以下顺序导入:

  1. 平台自带的种子数据(镜像内的 /app/platform_data)。只在平台版本(AppVersion)变化时重新导入;开发环境(GRAILS_ENV=development)、设置了环境变量 FORCE_RELOAD_SEED_DATA=true、或系统配置 debugDynamicLogic.forceRefreshDomainDefinition 为 true 时每次都导入。
  2. 应用的种子数据目录(见种子数据目录):先导入 plugins 子目录中的插件包,再导入 csv 子目录中的 CSV 文件。

每个 CSV 文件导入前都会做文件更新判断,内容没有变化的文件会被跳过,所以修改 CSV 后重启后端服务即可让修改生效。

# 插件数据导入

插件包是一个 zip 文件,根目录下包含:

路径 说明
PLUGIN_INFO 插件信息,JSON 格式,包含 name、version、description、dependsOnPlugins(依赖的插件名及最低版本)
csv/ 插件的种子数据 CSV
libs/ 插件的 jar 包
sql/ 可选,导入前后执行的 SQL,见导入前后执行 SQL

插件可以通过两种方式导入:

  • 放在种子数据目录的 plugins 子目录下,系统启动时自动导入。
  • 在「开发定制 > 动态插件」列表上使用「Import dynamic plugin」动作上传(需要 DEVELOPER 角色)。

「Import dynamic plugin」动作的参数窗口中有三项(界面上显示为英文):

参数 说明
Plugin file 要导入的插件 zip 包
Ignore MD5 Check 打开后不做文件更新判断,插件中的每个 CSV 文件都重新导入。只在插件包实际被导入时有效:系统中已有同名插件、新插件版本不更高、文件名不含 SNAPSHOT 且没有打开 Overwrite Conflict 时,整个插件包被忽略,打开这一项也不会导入任何 CSV(见下方导入规则)
Overwrite Conflict 打开后,不论版本高低都用这个插件包覆盖系统中的同名插件,插件中的 CSV 导入时也跳过冲突检测,直接覆盖数据库中的值(见冲突检测与覆盖)

导入规则:

  • 多个插件按 PLUGIN_INFO 中声明的依赖关系排序,被依赖的插件先导入;依赖的插件不存在、版本不满足或存在循环依赖时导入失败。
  • 系统中已有同名插件时,只有新插件的版本更高、插件文件名中包含 SNAPSHOT,或者打开了 Overwrite Conflict 时才会覆盖;否则忽略这个插件包。
  • 插件中的 CSV 同样按导入顺序导入,但 HierarchyRole、DynamicLogicEngine、DynamicLogicType、DynamicLogic、RoleRequirement、DomainClass、DomainClassField、ExtendedDomainClass 会排在最前面。
  • 文件名包含 SNAPSHOT 的插件,效果与打开 Overwrite Conflict 相同:强制覆盖同名插件,导入 CSV 时强制覆盖冲突。

插件的开发和打包方式见 插件开发。

# 在列表页导入数据

平台内置了一个对象动作 Import domain data from csv(界面上显示为「导入数据」),可以把 CSV 文件导入到某个列表表单对应的模型中。这个动作默认没有绑定到任何表单,所以默认界面上看不到导入按钮,需要先把它绑定到目标列表表单:

  1. 打开「开发定制 > 对象动作 > DynamicActionDynamicForm」,点击「创建」。
  2. action 选「导入数据」,form 选目标列表表单,填写 displaySequence 后保存。

下面以平台内置的用户组为例,把「导入数据」绑定到用户组的列表表单 List of groups:

把导入数据动作绑定到用户组列表

注意

1.0.0-beta18 中,这个创建窗口的 dateCreated、lastUpdated 两个字段被标为必填,需要随便选一个时间(例如点「此刻」)才能保存,保存时系统会用实际时间覆盖。也可以用 CSV 绑定,见 CSV 导入模板 中的 DynamicActionDynamicForm 一节。

绑定后,「业务配置 > 用户组」列表上方出现「导入数据」按钮。准备一个 CSV 文件,例如 Group.csv:

name(*),roles.name[:]
市场部,[ROLE_USER]
客服部,[ROLE_USER]
1
2
3

点击「导入数据」,在上传框中选择一个或多个 CSV 文件,设置是否打开 Overwrite Conflict(覆盖冲突),点击「提交」。上传框的标题显示为「Plugin files」,这是当前版本的界面文案问题,这里选择的就是 CSV 文件:

导入数据对话框

导入完成后刷新列表,可以看到新建的两个用户组(列标题「User Groups」未翻译是当前版本的界面问题):

导入后的用户组列表
  • 导入的模型由列表表单的「关联对象类型」决定,与文件名无关。
  • 动作异步执行,完成后系统发送消息通知,导入结果在「执行记录 > 数据导入」中查看。
  • 这种方式不做文件更新判断,每次提交都会导入。
  • 打开 Overwrite Conflict 时,冲突的行也会被 CSV 覆盖。

1.0.0-beta18 已知问题

  • 向动态模型(在「开发定制 > 领域模型」中类型为 DYNAMIC 的模型,例如手把手教程中的客户、销售机会)导入数据时,当前每一行都会失败,失败原因为 ScopedValue not bound。系统启动时自动导入和「导入数据」动作都受影响;平台内置模型(如用户、用户组、表单、菜单)不受影响,上面的例子因此使用用户组。
  • 即使所有行都导入失败,「导入数据」动作仍然提示「执行成功」,请以「执行记录 > 数据导入」中的状态为准。提交后的结果窗口里,「Execution record: <a href='/DynamicActionExecRecord/…'…」这段 HTML 原样显示为文本,没有渲染成链接;执行记录可以从随后收到的系统消息中的「执行日志」链接打开。
  • 在界面上,只有 DEVELOPER 及以上角色能使用「导入数据」:动作的参数表单 Import domain data from csv RequestForm 要求 DEVELOPER 角色,普通用户点击按钮后打不开参数表单(接口返回 403),导入结果所在的「执行记录 > 数据导入」也需要 DEVELOPER。动作定义本身只要求 USER 角色,这只在直接调用动作接口时起作用;无论哪种方式,导入过程都不检查目标模型的新建、修改、删除权限。

# 种子数据目录

在 application.yml 中通过 seedData.folder 配置应用种子数据的根目录,默认读取环境变量 SEED_DATA_FOLDER,未设置时为 /app/data:

seedData:
  folder: ${SEED_DATA_FOLDER:/app/data}
1
2

以平台提供的 docker compose 开发工程为例,SEED_DATA_FOLDER 设置为 /app/plugin/data,工程中的 codes/data 下各子目录分别挂载到该目录下。

目录结构如下:

<seedData.folder>
├── Tenant.csv            --> 可选,系统首次启动时创建的租户列表
├── csv                   --> 种子数据 CSV 文件,文件名规则见「文件名称」
│   ├── DomainClass.csv
│   ├── DomainClassField.csv
│   └── ...
├── plugins               --> 可选,启动时自动导入的插件包
├── sql                   --> 可选
│   ├── before_import.sql --> 导入 CSV 之前执行
│   └── after_import.sql  --> 导入 CSV 之后执行
├── groovy                --> 一般放动态逻辑的源代码,由 CSV 中的 code(F) 列引用
├── css                   --> 一般放显示主题的 CSS,由 DynamicTheme 的 css(F) 列引用
└── attachments           --> 一般放需要导入的附件,如显示主题用到的 logo
1
2
3
4
5
6
7
8
9
10
11
12
13

groovy、css、attachments 只是约定俗成的目录名,CSV 中以文件路径引用它们,路径从种子数据根目录算起。

提示

目录下不再按运行环境、租户分子目录。Tenant.csv 只有一列 name(*),只在没有通过系统属性 gorm.tenantId(docker 镜像中由环境变量 TENANT_ID 设置)指定租户时使用,用于创建数据库中还不存在的租户。

# 导入顺序

一个目录中的 CSV 文件按以下顺序导入:

  1. 带前缀的文件,例如 000-DynamicLogicEngine.csv、006-DomainClass.csv,按文件名排序后最先导入。判断规则是:文件名中第一个 _ 之前的部分恰好含一个 -,- 前面是前缀,后面是模型名。前缀一般用数字,便于排序。
  2. 预定义顺序的模型:DisplayComponentModule、DynamicLogicEngine、DynamicLogicType、DynamicConfig、DynamicLogic、HierarchyRole、RoleRequirement、DynamicObjectHook、DomainClass、DomainClassField、DynamicFormType、DynamicForm、DynamicMenu、DynamicActionGroup、DynamicAction,按这个顺序导入。
  3. 其余文件:根据模型之间的关联关系自动排序,被依赖的模型先导入。例如同时存在 Group.csv、User.csv、UserGroup.csv 时,UserGroup.csv 在另外两个之后导入。

如果自动识别的依赖关系不满足需要,可以在模型定义中用 loadAfter 指定:

  • 动态模型:在领域模型的扩展信息(extInfo)中设置 loadAfter,详见动态领域模型。
  • 平台内置模型(GORM 模型):在类中定义静态属性 loadAfter,例如 DynamicAction:
  // 表示在导入 DynamicAction 之前,需要先导入 DynamicLogic 和 DynamicActionGroup
  static loadAfter = [DynamicLogic, DynamicActionGroup]
1
2

如果某种对象不依赖其他对象、应尽早导入,可以把 loadAfter 设为空数组,例如 I18nType:

  static loadAfter = []
1

# 文件更新判断

导入某个 CSV 文件前,系统计算文件内容的 md5,并查找这个模型的导入记录中 md5 相同、已经结束的最近一条记录。如果该记录的状态是「成功」或「无数据导入」,就跳过这个文件。

提示

  • 判断依据是文件内容,与文件名、修改时间无关,加一行注释也会让文件重新导入。
  • 系统只看 md5 相同的记录中最近结束的那一条:它是「成功」或「无数据导入」就跳过,是失败或部分失败就重新导入。所以把文件改回以前某个成功导入过的版本时,它不会被重新导入,这时可以加一行注释改变文件内容。
  • 上次导入失败或部分失败的文件,下次启动时会整个文件重新导入。

# 导入前后执行 SQL

如果种子数据根目录(插件则是插件包根目录)下存在 sql/before_import.sql,系统在导入 CSV 之前执行它;存在 sql/after_import.sql 时,在导入 CSV 之后执行。

  • SQL 中的 ${TENANT} 会被替换为当前租户标识。
  • 每个租户都会执行一次。
  • SQL 执行失败只记录错误日志,不会中断导入。

# CSV 文件格式

以下章节描述了 CSV 文件数据准备相关的内容。

# 文件名称

文件名决定导入的模型:取文件名中第一个 _ 之前的部分,再去掉 - 及其之前的数字前缀,得到模型的简略名称(不含包名)。例如以下文件都导入到 DynamicForm:

  • DynamicForm.csv
  • DynamicForm_crm.csv
  • 011-DynamicForm_action.csv

.csv 后缀需要小写。动态模型使用领域模型的简略名称,例如 Customers_2026.csv。

# 标题行

所有待导入的 CSV 文件中,第一行均应该是标题行,标题行描述了该 CSV 文件中数据的结构。以下是一个标题行的示例:

username(*),password,name,accountLocked,DELETE_FLAG
1
  • 普通列的名称是模型中的字段名。
  • 后缀 (*) 的列,如 username(*),是查询字段,见查询字段。
  • 包含 . 的列,如 group.name,是关联对象字段,见关联对象的查询。
  • 后缀 (F) 的列,如 code(F),表示该列的值是一个文件路径,导入时读取文件内容作为字段值。路径从种子数据根目录(插件则是插件包根目录)算起。
  • 名为 DELETE_FLAG 的列用于删除数据,见对象删除。
  • 名为 OVERWRITE_FLAG 的列用于强制覆盖,见冲突检测与覆盖。
  • 如果某字段的类型是附件(StorageFieldValue),该列的值为待导入附件的文件路径。

注意

标题行中出现模型里不存在的列名时,整个文件导入失败;系统启动时自动导入的情况下,同一目录中排在它后面的文件以及 after_import.sql 也不会再执行。1.0 以前的动态字段列(以 (#) 结尾)1.0 起已移除,出现时同样会导致整个文件失败。

# 注释行

以英文分号(;)开头的行是注释行,导入时会被忽略:

name(*),implies.name[:]
;; 销售角色,包含普通用户角色
ROLE_SALES,[ROLE_USER]
1
2
3

# 查询字段

为了支持通过 CSV 文件创建或更新现有记录,所有在标题行中后缀是 (*) 的列会被识别为查询字段。系统用所有查询字段的值,按严格相等查询现有记录:

  • 查询结果为空:新建一条数据,导入记录中该行记为新建。
  • 查询结果只有一条:用 CSV 中的值更新这条数据,导入记录中该行记为更新;如果值没有变化,记为无更新。
  • 查询结果超过一条:跳过该行,导入记录中该行记为跳过。

提示

如果导入的 CSV 文件中没有任何查询字段,则更新和删除都不可用,所有记录都按新建处理。

# 关联对象的查询

如 group.name,表示导入该对象的 group 字段(一个 Group 对象)时,用 CSV 中该列的值去匹配 Group 的 name 字段,把查到的对象关联起来:

  • 查不到关联对象:该行导入失败。
  • 查到一个:继续该行的导入。
  • 查到多个:该行导入失败。

关联字段也可以作为查询字段,如 user.username(*)。

对于一对多、多对多的集合字段,值写成 [值1,值2],列名写成 roles.name[] 或 roles.name([] 可以省略,默认用逗号分隔;值中含逗号,整格要用双引号包起来)。也可以在方括号中指定分隔符,例如 roles.name[:] 表示用 : 分隔,值写成 [ROLE_A:ROLE_B],这样就不需要引号。

如果列名只写了字段名、没有写查询字段:

  • 单个关联对象(例如 customer):系统使用关联模型扩展信息中的 queryField 作为查询字段;关联模型也没有配置 queryField 时,该列的值无法解析,对应的行导入失败。
  • 集合字段(例如 roles):默认按关联对象的 name 字段查找。

注意

导入某对象时,它所依赖的关联对象必须已经在系统中存在,否则该行导入会失败。

# 空值

单元格为空,或填写字面量 NULL,该字段都按空值(null)导入。

# 特殊字符的转义

  • 如果某列的内容中包含英文逗号(,)或换行,该列需要用英文双引号(")包裹起来。
  • 被双引号包裹的内容中,英文双引号写两遍("")表示一个双引号,也可以用反斜杠转义(\")。例如 JSON 值 {"labelField": "name"} 在 CSV 中写作 "{""labelField"": ""name""}"。
  • 反斜杠(\)是转义符:\\ 表示一个反斜杠,\n、\t 会被转换成换行和制表符,反斜杠后面跟其他字符时原样保留(例如 C:\data 不变)。值中需要保留 \n 这样的字面内容时,把反斜杠写成 \\。

# 空白字符处理

在从 CSV 文件中读取数据时,系统会自动去掉每列前后的空白字符。

# 对象删除

CSV 文件导入时,支持删除现有数据,方法是:

  • 在 CSV 文件的标题行中增加 DELETE_FLAG 列;
  • 对于要删除的行,将其 DELETE_FLAG 列的值设置为 Y(取值规则同布尔值)。

注意

CSV 文件的标题行中必须有能唯一确定一条记录的查询字段,删除才能正确工作。

被标记为删除的行:

  • 查到 0 条现有数据:记录一条警告,不做任何操作。
  • 查到多条现有数据:跳过这一行。
  • 文件中没有查询字段:所有标记为删除的行都不会删除任何数据。

# 冲突检测与覆盖

每一行导入成功(新建或更新)后,系统会把该行的原始内容记录到「执行记录 > 行导入成功记录」中。之后再次导入同一条数据时,系统比较三份内容:数据库中的当前值、上次导入的 CSV 行、本次的 CSV 行。

以覆盖冲突方式导入时(「导入数据」或「Import dynamic plugin」动作中打开了 Overwrite Conflict,或插件文件名包含 SNAPSHOT),导入成功的行不会写入行导入成功记录。下一次以普通方式导入时,系统比较的仍是更早那次导入留下的记录。

界面上改过 CSV 改过 结果
否 是 正常更新
是 否 跳过,保留界面上的修改(导入记录中计入「数据库更新但 CSV 未更新」)
是 是 判定为冲突,不更新,该行记入冲突行

以下情况会跳过冲突检测,直接用 CSV 覆盖数据库中的值:

  • 该行的 OVERWRITE_FLAG 列值为 Y;
  • 「导入数据」动作中打开了 Overwrite Conflict;
  • 插件包的文件名中包含 SNAPSHOT。

提示

从来没有通过 CSV 导入过的数据(例如在界面上新建的表单),第一次用 CSV 更新时没有上次导入的记录,不会判定为冲突。

# 数据类型映射

如下列出了平台内置模型的字段类型与 CSV 中值的转换关系:

字段类型 CSV 中的值
String 原样导入
Boolean 见布尔值
Integer、Long、Double、BigDecimal 数字
java.time.LocalDate、LocalDateTime、ZonedDateTime、OffsetDateTime、java.util.Date 见日期及时间
枚举 见枚举类型
org.springframework.http.HttpMethod GET、POST 等
tech.muyan.storage.StorageFieldValue 附件的文件路径
关联对象 见关联对象的查询
Set、List 集合 [值1,值2],见关联对象的查询

动态模型的字段按领域模型字段的数据类型(dataType)转换,规则相同。

平台不提供注册自定义类型转换的扩展点。

# Boolean

对于 Boolean 类型的字段,值的对应关系如下,不在范围内的值会导致该行导入失败:

CSV 文件中的值 导入后的值
Y, y, Yes, YES, true, TRUE, T, t, 是, 1 true
N, n, No, NO, false, FALSE, F, f, 否, 0 false

# 日期及时间

支持以下格式,格式中各字母的含义可参考 SimpleDateFormat (opens new window):

 "yyyyMMdd"
 "dd-MM-yyyy"
 "yyyy-MM-dd"
 "MM/dd/yyyy"
 "yyyy/MM/dd"
 "dd MMM yyyy"
 "dd MMMM yyyy"
 "yyyyMMddHHmm"
 "yyyyMMdd HHmm"
 "dd-MM-yyyy HH:mm"
 "yyyy-MM-dd HH:mm"
 "MM/dd/yyyy HH:mm"
 "yyyy/MM/dd HH:mm"
 "dd MMM yyyy HH:mm"
 "dd MMMM yyyy HH:mm"
 "yyyyMMddHHmmss"
 "yyyyMMdd HHmmss"
 "dd-MM-yyyy HH:mm:ss"
 "yyyy-MM-dd HH:mm:ss"
 "MM/dd/yyyy HH:mm:ss"
 "yyyy/MM/dd HH:mm:ss"
 "dd MMM yyyy HH:mm:ss"
 "dd MMMM yyyy HH:mm:ss"
 "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 枚举类型

枚举字段只接受枚举常量名,并且大小写必须完全一致,例如 DynamicMenu 的 type 列写 FORM,而不是界面上显示的 Form。其他值会导致该行导入失败。

# 附件类型

对于附件类型的列,填写从种子数据根目录起算的相对文件路径,导入时系统会把该文件保存为附件(StorageFieldValue),并与导入的对象关联。

# 查看导入记录

每个 CSV 文件的导入过程都会记录在「执行记录 > 数据导入」中,查看需要 DEVELOPER 角色。下图第一行是上面导入用户组的记录:

数据导入记录列表

导入记录中保存了以下信息:

  • 导入对象的类型、导入 CSV 文件的 md5、标题行
  • 导入状态,取值:
    • 成功(SUCCESS)
    • 部分失败(PARTIALLY_FAIL)
    • 失败(FAILED)
    • 运行中(RUNNING)
    • 无数据导入(NO_DATA_IMPORTED)
    • 未开始(NOT_START)
    • 部分成功(PARTIALLY_SUCCESS,已废弃)
  • 导入开始、结束时间
  • 新建、更新、删除、无更新、冲突、失败、「数据库更新但 CSV 未更新」的对象 id 及数量
  • 失败行、跳过行、冲突行、「数据库更新但 CSV 未更新」的行在 CSV 中的原始内容
  • 导入过程的日志

注意

1.0.0-beta18 中,数据导入列表的「详情」列显示为 [object Object],点击行上的「详情」会导致页面报错;状态显示为英文(Success、Failed 等)。失败原因可以在列表的「失败行」「过程日志」列中查看。

另外,数据导入列表的「运行开始」「运行结束」按 UTC 原样显示,没有换算成浏览器所在时区;下面「行导入成功记录」中的「导入时间」则已换算成本地时间。在东八区查看时,同一次导入在两个列表中的时间相差 8 小时(上图第一行的 16:16 与下图前两行的 00:16 是同一次导入)。

每一行导入成功的记录保存在「执行记录 > 行导入成功记录」中,冲突检测依赖这些记录:

行导入成功记录

# 一个完整的例子

下面的例子新建一个销售角色、一个销售部用户组和两个销售人员,并把人员加入用户组。在 csv 目录下新建以下文件(文件名后缀 _crm 可以任取):

HierarchyRole_crm.csv:

name(*),implies.name[:]
;; 销售角色,包含普通用户角色
ROLE_SALES,[ROLE_USER]
1
2
3

RoleRequirement_crm.csv:

name(*),hasPermissionRoles.name[:],customLogic.name
SALES,[ROLE_SALES],
1
2

Group_crm.csv:

name(*),roles.name[:]
销售部,[ROLE_SALES]
1
2

User_crm.csv:

username(*),password,name,accountLocked,DELETE_FLAG
[email protected],Crm@2026,王芳,N,N
[email protected],Crm@2026,刘洋,否,N
1
2
3

UserGroup_crm.csv:

user.username(*),group.name(*)
[email protected],销售部
[email protected],销售部
1
2
3

重启后端服务后,导入顺序为:HierarchyRole_crm.csv、RoleRequirement_crm.csv(预定义顺序),然后是 Group_crm.csv、User_crm.csv、UserGroup_crm.csv(按依赖关系排序,UserGroup 依赖 User 和 Group,排在最后)。

# 文件说明

  • 文件名到模型的映射:User_crm.csv 中第一个 _ 之前是 User,所以导入到 User 模型;后缀 _crm 只用于区分文件。
  • 现有记录查找:User_crm.csv 中只有 username(*) 是查询字段,按用户名查找现有用户,找到就更新,找不到就新建;UserGroup_crm.csv 中 user.username(*) 和 group.name(*) 两个关联字段同时作为查询字段。
  • 关联对象查询:group.name(*) 表示用 Group 的 name 字段匹配用户组。

# 字段说明

  • implies.name[:]、roles.name[:]、hasPermissionRoles.name[:]
    • 集合字段,值写在方括号中,多个值用 : 分隔。例如 [ROLE_SALES] 表示关联名为 ROLE_SALES 的角色。
    • 关联的对象必须已经存在:ROLE_SALES 在 HierarchyRole_crm.csv 中创建,它按预定义顺序先于 Group_crm.csv 导入。
  • accountLocked
    • Boolean 字段,N 和 否 都会被转换为 false,见布尔值。
  • password
    • 用户的初始密码,导入时系统会加密保存到用户表(注意:CSV 行原文,包括明文初始密码,会保存在「执行记录 > 行导入成功记录」中,DEVELOPER 及以上角色可以看到;请只用临时初始密码,并要求用户首次登录后修改)。
  • DELETE_FLAG
    • 值为 N,表示执行新建或更新。要删除某个用户时,把该行的值改为 Y 后重新导入。
  • customLogic.name
    • 留空,表示该角色要求不使用自定义逻辑,只按角色判断。
Last Updated: 2026/9/24 14:27:35