# 平台 API

平台对外提供两类 API:

  1. REST API:通过 HTTP 调用平台,供前端、移动端、第三方系统和脚本使用。
  2. 插件工具类库(tech.muyan:api):在插件源码里调用平台能力的 Java/Groovy 工具类,例如查询数据、创建对象、异步执行。

# 目录

  1. REST API
  2. 插件工具类库

# REST API

# 地址前缀

按 Docker 部署 一文部署时,前端和后端由同一个 nginx 对外提供服务,nginx 会把 /api/ 开头的请求去掉 /api 前缀后转发给后端。所以从外部调用时,下文所有路径前面都要加上 /api,例如登录接口的完整地址是 http://<服务器地址>/api/auth/login。

提示

动态逻辑里拿到的请求地址(例如 Webhook 的 requestUrl)是后端看到的地址,不带 /api 前缀。

# 登录

curl -X POST http://<服务器地址>/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"[email protected]","password":"password"}'
1
2
3

登录成功返回 HTTP 200,响应体如下(token 已截断):

{
  "access_token": "eyJhbGciOiJIUzI1NiJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "username": "[email protected]",
  "roles": ["ROLE_DEVELOPER", "ROLE_ADMIN", "ROLE_USER"],
  "id": 1,
  "name": "超级管理员",
  "avatar": null,
  "extInfo": null
}
1
2
3
4
5
6
7
8
9
10
11
12
  • access_token:访问令牌,默认有效期 3600 秒(expires_in),可以通过后端配置项 security.jwt.expiration 调整。
  • refresh_token:刷新令牌,用于换取新的访问令牌,见 刷新 token。

密码错误时返回 HTTP 400,用户不存在时返回 HTTP 404:

{"success":false,"errCode":1010,"errMsg":"ErrorCode: 1010, ErrorMsg: UserPasswordMismatch, ..."}
{"success":false,"errCode":1005,"errMsg":"ErrorCode: 1005, ErrorMsg: NotFound, ..."}
1
2

# 在请求中携带 token

后端根据请求的特征,从以下来源中选一个读取 token:

优先级 方式 示例
1 请求头 Authorization(推荐) Authorization: Bearer <access_token>
2 表单编码请求体中的 access_token Content-Type: application/x-www-form-urlencoded,请求体 access_token=<token>&...
3 URL 参数 access_token GET /api/data/DynamicLogic?access_token=<token>
4 Cookie access_token Cookie: access_token=<token>
curl 'http://<服务器地址>/api/data/DynamicLogic?max=10&offset=0' \
  -H 'Authorization: Bearer <access_token>'
1
2

注意

  • 当前版本中,第 2 种方式(表单编码请求体)实测不可用:请求会返回 HTTP 400 IllegalArgument ... has an invalid signature。请使用其他三种方式。
  • 请求头 Authorization 以 Bearer 开头时,只读请求头;非 GET 的表单编码请求只读请求体,不会再看 URL 参数和 Cookie。
  • URL 参数中的 token 会出现在服务器访问日志里;调用动态服务时,还会原样写进服务执行记录的“执行参数”中。能用请求头就不要用 URL 参数。

认证失败时的返回:

  • 没有携带 token:要求登录的接口(如 /auth/userInfo、/permissions/...)返回 HTTP 401(errCode 1008 Unauthorized);按领域模型校验权限的接口(如 /data/...)返回 HTTP 403(errCode 1004 NoPermission)。
  • token 无法解析:HTTP 401(errCode 1012 TokenInvalid);签名不对:HTTP 400(errCode 1001 IllegalArgument)。
  • token 过期:HTTP 401(errCode 1006 TokenExpired),此时应先 刷新 token。

# 刷新 token

access_token 过期后,可以用 refresh_token 换取新的 access_token,不必重新输入密码。请求参数使用表单编码(不支持 JSON 请求体,JSON 请求体会返回 400):

curl -X POST http://<服务器地址>/api/auth/access_token \
  -d 'grant_type=refresh_token&refresh_token=<refresh_token>'
1
2

成功时返回与登录接口相同结构的响应,其中 refresh_token 仍是请求中传入的那个。

安全提示

当前版本的 refresh_token 没有设置过期时间(后端配置项 security.jwt.refreshExpiration 默认为空),并且可以直接当作 access_token 调用接口。另外,拿未过期的 access_token 调用 GET /auth/userInfo,响应里会带回一个新签发的、不过期的 refresh_token(/auth/access_token 则只会把传入的 token 原样返回为 refresh_token)。请像保管密码一样保管 refresh_token,不要写进日志、URL 或前端之外的存储里。

# 通过 URL 中的 token 临时访问

打开形如 http://<服务器地址>/?access_token=<access_token> 的地址,前端会直接用这个 token 访问系统,不需要登录页面。适合从其他系统跳转过来、免登录查看某个页面的场景。

这种方式下:

  • 前端不会把 token 写进浏览器的 localStorage,关闭页面后不会留下登录状态。
  • 前端打开页面时会调用 GET /auth/userInfo。这个接口会用 URL 里的 token 重新签发一对新的 access_token 和 refresh_token,前端在页面打开期间用它们续期。

风险

URL 里的 token 不是一次性的。任何拿到这个链接的人,都可以在 token 过期前调用 /auth/userInfo 换到一个新的 refresh_token,而 refresh_token 当前不会过期(见上节)。所以:

  • 只为权限最小的专用账号生成这类链接,不要用管理员账号;
  • 链接只通过可信渠道发送,不要贴在公开页面、工单或聊天群里;
  • 当前版本登录和用 token 访问时都不检查账号是否被禁用或锁定,修改密码也不会让已签发的 token 失效。链接泄露后,需要删除该账号或修改其用户名;要让所有已签发的 token 失效,只能更换后端配置项 security.jwt.secret 并重启。

# 主要路由

下表列出平台内置的主要接口(路径均省略 /api 前缀)。<domain> 是领域模型的简称,例如 DynamicLogic。

方法 路径 说明
POST /auth/login 登录,见上文
POST /auth/access_token 用 refresh_token 换取新 token
GET /auth/userInfo 返回当前用户信息,并签发一对新 token
POST /auth/logout 退出登录。当前版本调用返回 403,不可用
GET /data/<domain>?max=&offset= 分页查询对象列表,返回 {total, data, formHookDataList}。max 默认 10,最大 500
GET /data/<domain>/<id> 查询单个对象,返回 {data, formHookData},对象在 data 里
POST /data/<domain> 创建对象,请求体为 JSON
PUT /data/<domain>/<id> 修改对象,请求体为 JSON,只需要包含 id 和要修改的字段
PUT /data/<domain>/batch 批量修改,请求体为 JSON 数组
DELETE /data/<domain>/<id> 删除对象
POST /search/<domain>?offset=&max= 按条件查询对象列表
POST /search/keyword/<domain>?q=&max= 按关键字查询,关键字放在 URL 参数 q 中
POST /action/byName/<动作名> 按名称执行对象动作
POST /action/byId/<动作 id> 按 id 执行对象动作
GET /permissions/<domain>/create 当前用户能否创建该类对象,返回 {"create": true}
POST /permissions/<domain>/ 请求体 {"ids":[...]},返回每个对象的 view/update/delete 权限
任意 /service/<服务名> 调用动态服务,见 动态服务
GET / POST /webhook/<url> 调用 Webhook,见 Webhook
GET / POST /attachment/... 附件上传、下载
GET /dashboard/list、/dashboard/meta/<id>、/dashboard/widget/data/<id> 仪表盘及其小组件数据
GET /config/system、/config/<key> 读取系统配置

创建和修改对象的示例(示例中的 id 都来自具体环境:logicType 12 是 DYNAMIC_SERVICE_CORE_LOGIC,dynamicLogicEngine 1 是 GROOVY_CODE,93 是新建出来的动态逻辑。请先用 GET /api/data/DynamicLogicType、GET /api/data/DynamicLogicEngine 查出自己环境里的 id):

# 创建
curl -X POST http://<服务器地址>/api/data/DynamicLogic \
  -H 'Authorization: Bearer <access_token>' -H 'Content-Type: application/json' \
  -d '{"name":"查询设备运行状态","description":"按设备编号返回设备运行状态","logicType":{"id":12},"dynamicLogicEngine":{"id":1},"code":"return [:]"}'

# 修改:只传 id 和要修改的字段
curl -X PUT http://<服务器地址>/api/data/DynamicLogic/93 \
  -H 'Authorization: Bearer <access_token>' -H 'Content-Type: application/json' \
  -d '{"id":93,"description":"按设备编号返回设备运行状态和累计运行时长"}'
1
2
3
4
5
6
7
8
9

创建、修改、删除接口的响应格式为 {"status": "...", "message": "...", "data": {...}, "date": <毫秒时间戳>}。注意单个对象的创建、修改在数据校验失败(例如必填字段为空)时 HTTP 状态码仍是 200,只是 status 为 error、message 为校验错误信息,调用方要检查 status 字段,不能只看 HTTP 状态码。批量修改接口 PUT /data/<domain>/batch 不同:任意一条校验失败都会直接抛出异常,返回错误响应。

对象类型的字段用 {"id": <id>} 表示。增删改查受领域模型上配置的角色要求控制,见 对象权限控制。

提示

路由表中已经存在 PATCH /data/<domain>/<id>,但后端没有对应的实现,调用会返回 403,请使用 PUT。

除上表外,插件还可以通过 自定义 Controller 注册自己的接口。

# 插件工具类库

插件源码通过 tech.muyan:api 这个库调用平台能力。库里的工具类是桩实现:方法体只会抛出 IllegalStateException(“This method will be provided by platform implementation dynamically”),真正的实现由平台在运行时提供。因此它必须以 compileOnly 方式引入,不能打进插件包,否则调用时会直接抛异常。插件模板的 codes/build.gradle 已经配置好:

dependencies {
  compileOnly 'tech.muyan:api:0.0.5'
  testCompileOnly 'tech.muyan:api:0.0.5'
  testRuntimeOnly 'tech.muyan:api:0.0.5'
}
1
2
3
4
5

版本说明

  • 插件模板默认使用 0.0.5。本节列出的方法均以 0.0.5 为准,并在当前平台(1.0.0-beta18)上确认过运行时存在。
  • 自定义 Controller(MuyanDynamicController、@Get/@Post/@Put/@Delete 等注解)和动态 RPC(MuyanRpcService、@RpcClient、DynamicRpcClientService)只在 1.0.0 系列的 api 库里提供,例如 libs-snapshot 仓库中的 1.0.0-11-35-SNAPSHOT,0.0.5 里没有。1.0.0 系列 api 库的 AsyncHelper 与平台运行时一致(多了 task(String, Runnable)、supplyAsync 等),但其中的 MessageHelper 仍然只声明了 pushNotification,同样不能用(见 MessageHelper)。
  • 库里有少数方法在当前平台运行时不存在,下文逐一标出,请不要使用。

目录:

  1. StorageUtils
  2. QueryHelper
  3. SimpleQuery
  4. DomainHelper
  5. BeanContainer
  6. AsyncHelper
  7. MessageSeverity
  8. MessageHelper
  9. 插件扩展接口

# StorageUtils

用于处理文件存储的工具类。

# createStorageFileDomain

public static StorageFieldValue createStorageFileDomain(String fileName, String mimeType, InputStream inputStream)
1

把一个文件保存到平台存储中,返回代表该文件的 StorageFieldValue 对象,可以赋给对象的文件字段。

  • 参数:
    • fileName:文件名
    • mimeType:文件的 MIME 类型
    • inputStream:文件内容
  • 返回: StorageFieldValue 对象

# QueryHelper

在数据库会话、事务中执行代码,或者直接执行 SQL。

public static <T> T withSession(Closure<T> closure)
public static <T> T withNewSession(Closure<T> closure)
public static <T> T withTransaction(Closure<T> closure)
public static <T> T withSql(Function<Sql, T> function)
1
2
3
4
  • withSession:在当前数据库会话中执行闭包。
  • withNewSession:在一个新的数据库会话中执行闭包。
  • withTransaction:在事务中执行闭包。
  • withSql:传入一个 groovy.sql.Sql 对象执行函数,适合执行原生 SQL。
import groovy.sql.Sql
import tech.muyan.utils.QueryHelper

List rows = QueryHelper.withSql { Sql sql ->
  sql.rows("select id, name from dynamic_logic where logic_type_id = ?", [12])
}
1
2
3
4
5
6

# SimpleQuery

用于按条件查询对象的查询类。

SimpleQuery.of("WorkTask")
    .ge("scheduledStartTime", start)
    .listAll();
1
2
3

此查询检索所有计划开始时间大于或等于指定开始时间的 WorkTask 对象。

可以把多个条件串在一起:

SimpleQuery.of("WorkTask")
    .eq("assignee", user)
    .ge("scheduledStartTime", start)
    .lt("scheduledEndTime", end)
    .eq("status", "ACTIVE")
    .listAll();
1
2
3
4
5
6

在上面的示例中:

  • "WorkTask" 是领域模型的简称。
  • "scheduledStartTime" 是领域模型中字段的名称。
  • "assignee" 是 WorkTask 的指派用户,匹配条件要用用户对象本身,而不是 user.id。

提示

对于对象类型 (DOMAIN_OBJECT) 的字段,应使用对象本身而不是 object.id 作为匹配条件。

# 条件方法

以下方法都返回 SimpleQuery 本身,可以链式调用:

方法 说明
eq(String fieldName, Object value) 等于
ne(String fieldName, Object value) 不等于
gt(String fieldName, Object value) 大于
ge(String fieldName, Object value) 大于等于
lt(String fieldName, Object value) 小于
le(String fieldName, Object value) 小于等于
iLike(String fieldName, String value) 不区分大小写的模糊匹配
notILike(String fieldName, String value) 不区分大小写的模糊不匹配
in(String fieldName, Collection<?> value) 在集合中
notIn(String fieldName, Collection<?> value) 不在集合中
isNull(String fieldName) 为空
notNull(String fieldName) 不为空
addConditions(List<QueryCondition> queryConditions) 一次加入多个条件

# 执行查询

方法 返回值 说明
get() T 返回单个结果
list(int offset, int limit) PaginationQueryResult<T> 分页查询
list(int offset, int limit, List<String> orderBy) PaginationQueryResult<T> 分页并排序
list(int offset, int limit, List<String> orderBy, boolean asc) PaginationQueryResult<T> 分页并指定排序方向
listAll() List<T> 返回全部结果

PaginationQueryResult<T> 继承自 List<T>,可以直接当列表使用。

# 静态方法

方法 说明
SimpleQuery<?> of(String domainName) 按领域模型简称创建查询
<T> SimpleQuery<T> of(Class<T> clazz) 按类创建查询
<T> SimpleQuery<T> of(Class<T> clazz, boolean and) 按类创建查询,并指定条件之间是与还是或
Object getById(String domainName, Long id) 按 id 查询
<T> T getById(Class<T> clazz, Long id) 按 id 查询
List<Object> getByIds(String domainName, List<Long> ids) 按 id 列表查询
<T> List<T> getByIds(Class<T> clazz, List<Long> ids) 按 id 列表查询
List<Object> getAll(String domainName) 查询全部对象
<T> List<T> getAll(Class<T> clazz) 查询全部对象
long count(String domainName) 统计对象数量
<T> long count(Class<T> clazz) 统计对象数量

# DomainHelper

创建、修改、删除和渲染对象的工具类。

public static Object buildDomain(String domainName)
public static Object buildDomain(String domainName, Object properties)
public static void createDomain(Object requestData)
public static void updateDomain(Object requestData)
public static void deleteDomain(Object requestData)
public static Map<String, Object> render(Object domainObj, DomainObjectRenderType renderType)
public static <T> List<T> batchCreate(Class<T> domainClazz, List<T> domainObjects)
public static <T> List<T> batchCreate(String domainName, List<T> domainObjects)
1
2
3
4
5
6
7
8
  • buildDomain:按领域模型简称构造一个对象(不保存),可以同时传入属性。
  • createDomain / updateDomain / deleteDomain:按请求数据创建、修改、删除对象。
  • render:把对象转成 Map。DomainObjectRenderType 有两个值:ONLY_LABEL_FIELD(只取显示字段)、ALL_COLUMNS(取全部字段)。
  • batchCreate:批量创建对象,返回创建后的对象列表。

# BeanContainer

获取平台和插件中的 bean。

public static <T> T getBean(Class<T> beanClass)
public static <T> Collection<T> getBeansOfType(Class<T> beanClass)
1
2
  • getBean:按类型获取一个 bean。
  • getBeansOfType:按类型获取所有 bean。

BeanContainer 由平台内置插件 PlatformAdapter 在运行时提供,所以插件的 dependsOnPlugins 里必须保留 PlatformAdapter,见 插件开发。

# AsyncHelper

异步执行任务,并在新线程中保持当前的租户等上下文。

public static Runnable scheduleAtFixRate(long period, Runnable runnable)
public static Runnable scheduleAtFixRate(long period, Runnable runnable, boolean newThread)
public static void task(Runnable runnable)
1
2
3
  • scheduleAtFixRate:以固定间隔(毫秒)重复执行任务,返回值是一个 Runnable,执行它即可取消任务。newThread 为 true 时使用独立的计时线程。
  • task:异步执行一次任务。

两个方法在新线程中都带有当前租户和插件的类加载器,但没有当前登录用户。平台运行时已把这两个方法标为 @Deprecated(task(Runnable) 建议换成 1.0.0 系列 api 库里的 task(String threadName, Runnable runnable)),新代码尽量少用。

当前平台上不可用的方法

以下方法在 api 0.0.5 中有声明,但当前平台运行时没有对应实现,调用会报 NoSuchMethodError(Java 或 @CompileStatic 代码)或 MissingMethodException(动态 Groovy 代码):

  • task(boolean newThread, Runnable runnable)
  • task(ExecutorService executorService, Runnable runnable)
  • newForkJoinPool(int parallelism)

另外,平台运行时的 task(Runnable) 返回 Thread,与 0.0.5 声明的 void 不一致。在动态 Groovy 代码中调用没有问题;在 Java 或 @CompileStatic 代码中调用会报 NoSuchMethodError。

# MessageSeverity

表示消息严重级别的枚举。

枚举值 说明
INFO 普通信息
WARNING 警告
ERROR 错误
IMPORTANT 重要
INTERNAL 原意是系统内部信息交换使用。当前版本后端不做过滤,前端按普通信息(INFO)弹出通知
ACTION_REQUIRED 需要用户处理的消息。当前版本前端的显示方式与 IMPORTANT 相同:右下角一条不会自动关闭的通知,不会遮挡页面

# MessageHelper

当前不可用

api 库(0.0.5 和 1.0.0 系列)中声明的 pushNotification 方法在当前平台运行时不存在,调用会失败。平台运行时实际提供的是 pushMessage,而且其 Notification 的收件人字段是 toUser(用户对象),与 api 0.0.5 中的 toUserName(用户名)不一致。在平台修复之前,请不要在插件中调用 MessageHelper。

# 插件扩展接口

除上面的工具类外,tech.muyan.api 包中还有供插件实现的接口,例如:

  • DynamicDomainEntity:把插件中的 POJO 类与平台的领域模型绑定。
  • MuyanPlatformComponent:插件组件,平台加载、卸载插件时回调 onLoad() / offLoad()。
  • websocket.MuyanWebSocketComponent:插件提供 WebSocket 消息处理。

用法见 插件开发。

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