This commit is contained in:
oneao committed 2026-10-13 22:32:50 +08:00
1 parent f3936cec61
commit 15e0ae5b67
115 files changed
+11399 -2741

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
---
title: 登录与鉴权
description: 登录、凭证携带、会话顶替与登录日志
pageClass: g3-api
---
# 登录与鉴权
除 `/auth/login` 外,**所有接口都需要携带凭证**。
## `POST` `/auth/login`
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orgid` | string | 是 | 机构码,决定登录到**哪个机构的库** |
| `userid` | string | 是 | 用户账号(`b_user.b_id`) |
| `password` | string | 是 | 密码 |
</G3ApiParams>
<G3ApiPayload title="请求">
```bash
curl -X POST http://localhost:8088/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"orgid":"G3HD","userid":"g3soft","password":"******"}'
```
</G3ApiPayload>
<G3ApiPayload title="登录成功" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9...",
"orgid": "G3HD",
"user": {
"id": "g3soft",
"account": "g3soft",
"name": "G3Soft"
}
}
}
```
</G3ApiPayload>
<G3ApiPayload title="账号或密码错误" :code="401">
```json
{
"code": 401,
"message": "机构码、账号或密码错误",
"data": null
}
```
</G3ApiPayload>
**几点要注意**:
- **机构码会归一化**:去掉首尾空白并转大写,所以传 `g3hd` 与 `G3HD` 等价。
- **机构码决定数据源**:登录时按机构码加载该机构的数据库配置;机构码不存在或配置缺失会失败。
- **「账号不存在」与「密码错误」对外是同一个提示**(`机构码、账号或密码错误`),不区分——避免被用来探测账号是否存在。但**登录日志里会分别记录**真实原因,便于排查。
- **`user.id` 与 `user.account` 目前是同一个值**(字符串业务键方案:`b_user.b_id` 即账号本身)。
- 用户编码与机构码都会写进 JWT,后续请求据此路由数据源与判定权限。
## 携带凭证
所有后续请求在请求头带 Bearer token:
```bash
curl -X POST http://localhost:8088/api/data/page \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...' \
-d '{"view_name":"v_b_othercompany","order_by":"b_id ASC"}'
```
| 场景 | 结果 |
|---|---|
| 缺少或格式不对的 `Authorization` | `401` |
| token 无效 / 过期 / 签名不符 | `401` |
| token 有效但**会话已被顶替** | `40101` |
| `OPTIONS` 预检请求 | **放行**(不校验) |
| `/auth/login` | **放行**(唯一不需要凭证的业务接口) |
> **HTTP 状态码始终是 200**,鉴权失败靠响应体的 `code`(`401` / `40101`)判断。
> 不要用 HTTP 状态码或 axios 的错误拦截分支处理鉴权失败。
## 会话与顶替
服务端维护**活动会话表**,同一个「机构 + 账号」只保留一个有效会话:
```text
登录 → 生成 sessionId → 登记为活动会话
再次登录同一账号 → 旧会话被顶替
旧 token 继续用 → 返回 40101(账号已在其他设备登录)
```
**前端处理**:拿到 `401` 或 `40101` 都应**清 token、回登录页**。`40101` 建议给一句「账号已在其他设备登录」,比笼统的「登录已失效」更有助于用户理解。
> 会话是**内存态**:服务重启后全部失效,用户需重新登录。
## 登录日志
每次登录尝试都会记录,含成功与失败:
| 记录内容 | 说明 |
|---|---|
| 机构码、用户账号 | 尝试登录的身份 |
| 成功 / 失败、失败原因 | 「账号不存在」/「密码错误」 |
| 会话 ID | 登录成功时 |
| 客户端信息 | 浏览器、操作系统、设备类型 |
| IP 与归属地 | 来源地址解析 |
查询入口:`v_s_log_login` 视图(走通用查询接口)。日志表本身**不需要登录权限之外的特殊授权**,但建议只给管理员开放。
## 相关配置
| 配置项 | 说明 |
|---|---|
| `erp.auth.jwt-secret` | 签名密钥,**生产必须改**(默认值仅用于开发) |
| `erp.auth.jwt-expiration` | token 有效期,默认 `24h` |
| `erp.database.config-dir` | 机构数据库配置目录,登录时按机构码在此查找 |
## 常见错误
| 场景 | code | 说明 |
|---|---|---|
| 机构码为空 | `1000` | 「机构码不能为空」 |
| 用户 ID 为空 | `1000` | 「用户ID不能为空」 |
| 机构码、账号或密码错误 | `401` | 三者统一提示 |
| 机构码在配置目录里不存在 | `401` | 加载机构配置失败时按机构码错误处理 |
| 未带凭证访问其他接口 | `401` | —— |
| 会话被顶替 | `40101` | 需重新登录 |
+124
View File
@@ -0,0 +1,124 @@
---
title: 导出
description: 列表导出、行数统计与合计查询
pageClass: g3-api
---
# 导出
导出接口,前缀 `/export/**`。三个接口**走完全相同的权限链路与同一套聚合 SQL**,因此「共 N 条」、列表底部合计与导出文件里的内容必然一致。
| 接口 | 返回 | 用途 |
|---|---|---|
| `/export/list` | **文件流**(xlsx 或 zip) | 导出列表 |
| `/export/count` | JSON | 批量导出弹窗的「共 N 条」 |
| `/export/totals` | JSON | 列表底部的「总合计」 |
<G3ApiParams title="共同的请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `moduleId` | string | 是 | 模块编码(决定列、查询表与权限) |
| `searchCondition` | string | | 查询条件(布尔表达式片段) |
| `orderBy` | string | | 排序,如 `b_id ASC`,多列逗号分隔 |
| `rowKeys` | any[] | | 勾选行的主键;**传了就是「按勾选导出」** |
| `keyField` | string | | 配合 `rowKeys` 的主键列名 |
| `columns` | string[] | | 指定导出列(默认按模块配置) |
| `rangeStart` / `rangeEnd` | int | | 行号区间(1 起,闭区间),导出当前页/区间 |
| `volumeSize` | int | | 分卷大小;传了则返回 **zip**,按此条数分卷 |
| `withSummary` | bool | | 是否输出合计行,**默认 `true`** |
</G3ApiParams>
## `POST` `/export/list`
导出并直接返回文件流。
<G3ApiPayload title="请求体">
```json
{
"moduleId": "v_b_othercompany",
"searchCondition": "b_canuse = 1",
"orderBy": "b_id ASC",
"withSummary": true
}
```
</G3ApiPayload>
**响应不是 JSON**,而是文件流,按 `Content-Type` 区分:
| 场景 | Content-Type | 说明 |
|---|---|---|
| 普通导出 | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` | 单个 xlsx |
| 分卷导出(传了 `volumeSize`) | `application/zip` | 多个 xlsx 打包 |
响应头带 `Content-Disposition: attachment; filename*=UTF-8''...`(文件名已做 URL 编码,中文名可直接还原)。
**几种请求形态的差异**:
| 形态 | 传什么 | 结果 |
|---|---|---|
| 全部导出 | 不传 `rowKeys` / 区间 | 当前条件下的**全部行**(不是当前页) |
| 勾选导出 | `rowKeys` + `keyField` | 只导出选中的行 |
| 区间导出 | `rangeStart` / `rangeEnd` | 导出指定行号区间 |
| 分卷导出 | `volumeSize` | 按条数切成多个 xlsx,打包返回 |
> `rowKeys` 里的数字主键**按原值透传**,不做字符串化——否则数字主键变成字符串后索引失效。
## `POST` `/export/count`
符合条件的行数。请求字段同上(通常只传 `moduleId` / `searchCondition`)。
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": { "total": 128 }
}
```
</G3ApiPayload>
> 因为与导出共用同一条权限链路与同一套 WHERE,这个数字**与真正导出的行数一致**——可以直接用来做「共 N 条」提示。
## `POST` `/export/totals`
各合计列的聚合值。请求字段同上。
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": {
"b_amount": 123456.78,
"b_qty": 320
}
}
```
</G3ApiPayload>
返回的键是**列名**,值是聚合结果(`sum` / `count` 等,取决于模块里该列的配置)。
> **没有配置任何合计列时返回空对象** ``,不是错误——前端据此不渲染合计行。
## 错误与权限
- **权限过滤发生在 SQL 里**:导出按 `SqlPermissionService` 的三层判定裁剪字段与数据范围。
- **字段权限**:`b_view=0` 或 `b_export=0` 的列会被**移除**(不会导出空列)。
- **数据范围**:按 `s_user_data_power` 追加条件。
- **没有导出动作权限**时拒绝:`1000`「没有模块「x」的动作权限「export」,请联系管理员授权」。
- **参数错误也在写流之前抛出**,所以失败时响应体仍是 JSON(HTTP 200 + `code != 0`),**不会返回半截损坏的 xlsx**。前端可**按 `Content-Type` 判断**拿到的是文件还是错误 JSON。
| 场景 | 表现 |
|---|---|
| 模块不存在 | `1000`,`message` 含「模块」 |
| 缺少 `moduleId` | `1000`「moduleId 不能为空」 |
| `rangeStart` 等不是整数 | `1000`「rangeStart 必须是有效整数」 |
| 无导出权限 | `1000`「没有模块「x」的动作权限「export」」 |
+203
View File
@@ -0,0 +1,203 @@
---
title: 附件
description: 文件上传(本地 / OSS 直传)、记录落库、删除与静态访问
pageClass: g3-api
---
# 附件
文件接口,前缀 `/file/**`。有两种存储模式,由 `erp.file.storage-type` 决定(`local` / `aliyun-oss`)。
| 模式 | 上传链路 |
|---|---|
| `local` | 前端直接 POST 文件 → 后端写盘 + 落库 |
| `aliyun-oss` | 取凭证 → 前端直传 OSS → 回调后端落库 |
> **第一步先问模式**:调 `/file/config` 拿到当前模式,再决定走哪条链路。前端组件不要写死。
## `POST` `/file/config`
返回当前存储模式。
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": { "storageType": "local" }
}
```
</G3ApiPayload>
## `POST` `/file/upload`
**本地上传**(`storage-type=local` 时使用)。支持多文件,**是 `multipart/form-data`,不是 JSON**。
<G3ApiParams title="表单字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `files` | file[] | 是 | 文件,key 固定为 `files`,可重复 |
| `father` | string | 是 | 宿主业务主键(存宿主 `b_id`) |
| `moduleId` | string | 是 | 模块编码 |
| `cateId` | string | | 分类 ID |
</G3ApiParams>
<G3ApiPayload title="请求">
```bash
curl -X POST http://localhost:8088/api/file/upload \
-H 'Authorization: Bearer <token>' \
-F 'files=@report.pdf' \
-F 'father=1234567890' \
-F 'moduleId=v_b_othercompany' \
-F 'cateId=987654321'
```
</G3ApiPayload>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": [
{
"subid": "1234567890123",
"mx_filename": "report.pdf",
"mx_filesize": "20480",
"mx_fileext": "pdf",
"mx_mapfilename": "/api/file/static/G3HD/2026/10/13/abc.pdf",
"father": "1234567890",
"mx_moduleid": "v_b_othercompany",
"mx_cate_id": "987654321"
}
]
}
```
</G3ApiPayload>
- `mx_mapfilename` 就是**访问地址**,预览 / 下载直接用它。
- `subid` 是雪花 ID,**本地模式由后端自己取号**后写入 `bf_files`(前端不用管)。
## `POST` `/file/upload-credential`
**取 OSS 直传凭证**(`storage-type=aliyun-oss` 时使用)。无请求字段。
后端转发到 g3oss,`bucket` 即当前机构码。
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": {
"accessid": "LTAI...",
"policy": "eyJleHBpcmF0aW9uIjoi...",
"signature": "abc123==",
"dir": "G3HD/",
"host": "https://g3soft.oss-cn-hangzhou.aliyuncs.com",
"expire": 1700000000
}
}
```
</G3ApiPayload>
> **字段以 g3oss 实际返回为准**:后端只做 `readValue(...).Map` 的**原样透传**,不加工也不改名。
> 上面是 OSS 直传的常见字段形状,接入时请以实际返回为准;凭证有有效期,取到后应尽快直传。
## `POST` `/file/save-record`
**OSS 直传完成后回调落库**(`storage-type=aliyun-oss` 时使用)。本地模式不需要单独调它。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `subid` | number | 是 | 上传前取的雪花 ID |
| `father` | string | 是 | 宿主业务主键 |
| `mx_moduleid` | string | 是 | 模块编码 |
| `mx_cate_id` | string | | 分类 ID |
| `mx_filename` | string | 是 | 原始文件名 |
| `mx_filesize` | string | 是 | 文件大小(字节) |
| `mx_fileext` | string | 是 | 扩展名 |
| `mx_mapfilename` | string | 是 | OSS 返回的完整 URL |
</G3ApiParams>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": { "subid": "1234567890123", "message": "记录已保存" }
}
```
</G3ApiPayload>
> **`subid` 在 OSS 链路必须由前端在上传前取号**(调 `/data/nextid`),因为凭证直传时后端不在链路上,无法代为取号。
> 本地链路则相反:**后端自己取号**,前端不用传。两条链路都不能依赖数据库自增。
## `POST` `/file/delete`
删除文件:**先删存储、后删 DB 行**。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `subid` | string | 是 | 文件记录 ID |
</G3ApiParams>
<G3ApiPayload title="响应" :code="0">
```json
{ "code": 0, "message": "操作成功", "data": null }
```
</G3ApiPayload>
外部对象操作**不属于数据库事务**:
- 删存储失败 → **不删数据库行**(保持可重试)。
- 数据库删除失败 → 保留可重试的清理记录。
记录不存在时静默返回成功(幂等)。
## `GET` `/file/static/{fileName}`
**本地模式**下的文件访问入口。不是 JSON 接口,直接返回文件流。
```text
GET /api/file/static/G3HD/2026/10/13/abc.pdf
```
- 路径形如 `{orgId}/{yyyy}/{MM}/{dd}/{file}`,与 `mx_mapfilename` 去掉前缀后的部分对应。
- 带**路径穿越防护**:解析后必须仍在存储根目录内,否则 404。
- 响应头带 `Content-Disposition: inline`,浏览器可直接预览。
## 相关数据表
| 表 | 说明 |
|---|---|
| `bf_files` | 文件记录(主键 `subid` 雪花 ID,`father` 存宿主主键) |
| `bf_files_category` | 文件分类(`b_module_id` 为空表示跨模块通用分类) |
## 常见错误
| 场景 | 表现 |
|---|---|
| 本地上传时缺少 `files` | `1000` 参数错误 |
| 文件静态访问越界或不存在 | **HTTP 404**(这条路径不走统一响应格式) |
| OSS 凭证获取失败 | `1000`「获取 OSS 上传凭证失败: {状态码}」 |
| 存储模式不匹配 | 例如 `local` 模式下调 `/file/save-record` 会落库成功但文件并不存在——**要先查 `/file/config`** |
@@ -0,0 +1,382 @@
---
title: 流程运行
description: 送审、审批动作、转派加签、审批中心与进度查询
pageClass: g3-api
---
# 流程运行
运行侧接口,前缀 `/workflow/runtime/**`。前端**只能提交意图**:改状态、建实例、推进节点、写待办全部在后端一个事务里完成;前端不允许直接改 `wf_instance` / `wf_task` / `wf_cc` 或业务表状态。
流程的**读取**走通用接口(待办/已办/进度这类有专门的运行侧查询接口,见下)。
> 前置:模块要先有**启用的流程绑定**(见[流程配置](/api/workflow)),否则送审会直接报「没有匹配到流程绑定」。
## 送审
### `POST` `/workflow/runtime/submit`
按模块 + 业务事件匹配绑定,建实例并激活首个节点。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `moduleId` | string | 是 | 模块编码 |
| `businessId` | string | 是 | 业务主键(**按字符串传**,不依赖业务表主键类型) |
| `businessNo` | string | | 业务单号,仅用于展示 |
| `event` | string | | 业务事件,默认 `submit`;可选 `submit` / `resubmit` / `change` |
| `context` | object | | 业务字段,参与条件分支与审批人解析(如金额、部门) |
| `idempotencyKey` | string | | 幂等键,重复提交返回同一实例 |
</G3ApiParams>
<G3ApiPayload title="请求体">
```json
{
"moduleId": "v_b_othercompany",
"businessId": "1234567890",
"businessNo": "HT-2026-001",
"event": "submit",
"context": { "b_amount": 3200, "b_dept_id": "D002" },
"idempotencyKey": "submit-1234567890-v1"
}
```
</G3ApiPayload>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "已提交",
"data": {
"instanceId": "01a134fe-bde5-7058-a6b9-5f7e4f86b991",
"status": "running",
"currentSummary": { "summary": "待审批:部门负责人审批" },
"pendingTaskCount": 1,
"duplicated": false
}
}
```
</G3ApiPayload>
- `duplicated` 为 `true` 表示**这次请求被判为重复**(幂等键已存在),没有产生新副作用,`instanceId` 是既有实例。
- 送审时会把发起人、机构写进运行变量;审批人解析为空时实例直接进入 `error`,不会跳过节点。
## 审批动作
### `POST` `/workflow/runtime/task/action`
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | string | 是 | 待办任务 ID |
| `action` | string | 是 | `approve` / `reject` / `return` |
| `comment` | string | | 审批意见 |
| `idempotencyKey` | string | | 幂等键,重复审批只生效一次 |
</G3ApiParams>
三个动作的差异:
| action | 节点内是否立即生效 | 结果 |
|---|---|---|
| `approve` | 否,凑够应签人数才完成节点 | 节点完成后继续路由;全部走完则实例 `approved` |
| `reject` | 是,第一票即生效 | 按节点配置的驳回策略走:终止 / 退回 / 退回上一节点 |
| `return` | 是 | 直接退回发起人,实例 `returned` |
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "处理成功",
"data": {
"instanceId": "01a134fe-bde5-7058-a6b9-5f7e4f86b991",
"status": "running",
"currentSummary": { "summary": "审批中:部门负责人审批" },
"pendingTaskCount": 0,
"duplicated": false
}
}
```
</G3ApiPayload>
> **注意 `status` 是实例状态**,不是本次动作的结果。同一节点还有其他人待签时,状态仍是 `running`。
### `POST` `/workflow/runtime/task/claim`
抢占待办。仅当节点配置了 `allowClaim` 且调用者**确实是该任务的候选审批人**时可用。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | string | 是 | 待办任务 ID |
</G3ApiParams>
候选审批人取自任务生成时落下的**审批人快照**,不是当前任务表——所以组织架构后来变了也不会影响已产生任务的归属判定。不是候选人会被拒绝。
### `POST` `/workflow/runtime/task/transfer`
转交待办给他人。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | string | 是 | 待办任务 ID |
| `toUserId` | string | 是 | 接收人(必须是存在且启用的用户) |
| `comment` | string | | 转交说明 |
</G3ApiParams>
原任务被取消,**新建一条任务**给接收人,并记录转交动作与通知事件。
### `POST` `/workflow/runtime/task/add-approver`
加签:给当前节点追加审批人。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | string | 是 | 同节点任一任务,用于定位节点 |
| `userIds` | string[] | 是 | 追加的审批人 |
| `comment` | string | | 说明 |
</G3ApiParams>
已是该节点活动审批人的会被跳过;加签会自动顺延应签人数,不会让节点永远凑不满票。
### `POST` `/workflow/runtime/task/urge`
催办:**只发提醒,不改任务状态**(也不要求调用者是审批人,凭动作权限即可)。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | string | 是 | 待办任务 ID |
| `comment` | string | | 催办说明 |
</G3ApiParams>
## 实例操作
### `POST` `/workflow/runtime/instance/withdraw`
撤回。发起人撤回运行中的实例。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `instanceId` | string | 是 | 流程实例 ID |
| `comment` | string | | 撤回原因 |
</G3ApiParams>
- 只能撤回**运行中**的实例;实例进入 `withdrawn`,全部未完成待办被取消。
- 发起人撤回自己发起的实例不需要额外权限;非发起人需要 `withdraw` 动作权限。
- 撤回后重新提交会产生**新实例**与新快照。
### `POST` `/workflow/runtime/instance/cancel`
取消(管理员)。需要 `workflow_admin` 动作权限。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `instanceId` | string | 是 | 流程实例 ID |
| `comment` | string | | 取消原因 |
</G3ApiParams>
实例进入 `cancelled`,其中明原因会写入实例摘要与取消动作。
## 审批中心
### `POST` `/workflow/runtime/todo/list`
我的待办:分配给我(含已抢占)且未处理完的任务。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNo` | int | | 页码,默认 1 |
| `pageSize` | int | | 每页条数,默认 50,**上限 200** |
</G3ApiParams>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": {
"rows": [
{
"task_id": "01a134fe-...",
"node_key": "dept_manager",
"task_status": "pending",
"b_seq": 10,
"b_due_at": "2026-10-14T02:00:00Z",
"instance_id": "01a134ff-...",
"b_business_id": "1234567890",
"b_business_no": "HT-2026-001",
"instance_status": "running",
"initiator_name": "G3Soft",
"definition_name": "费用报销审批",
"node_name": "部门负责人审批"
}
],
"total": 1,
"pageNo": 1,
"pageSize": 50
}
}
```
</G3ApiPayload>
> **分页字段名是 `pageNo` / `pageSize`**,响应里也是这两个键(不是 `page` / `size`)。
> **排序**:按应处理时间升序,**没有时限的排在最后** —— 别拿第一行的 `b_due_at` 当作全表最小值。
### `POST` `/workflow/runtime/done/list`
我已处理:我实际处理过的任务(含通过、驳回、转交前的处理)。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNo` / `pageSize` | int | | 分页,默认 1 / 50,`pageSize` 上限 200 |
</G3ApiParams>
### `POST` `/workflow/runtime/instance/mine`
我发起的流程实例。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNo` / `pageSize` | int | | 分页,默认 1 / 50,`pageSize` 上限 200 |
</G3ApiParams>
### `POST` `/workflow/runtime/cc/list`
抄送我的。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `pageNo` / `pageSize` | int | | 分页,默认 1 / 50,`pageSize` 上限 200 |
</G3ApiParams>
### `POST` `/workflow/runtime/cc/read`
批量标记抄送已读。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ccIds` | string[] | 是 | 抄送记录 ID **数组**(注意是复数) |
</G3ApiParams>
<G3ApiPayload title="请求体">
```json
{ "ccIds": ["01a134fe-bde5-7058-a6b9-5f7e4f86b991"] }
```
</G3ApiPayload>
只允许标记**属于当前用户**的抄送记录,且会记录已读时间。数组为空时报 `1000`「请选择要标记的抄送记录」。
## 进度与详情
### `POST` `/workflow/runtime/instance/graph`
实例图:节点与连线的状态 + 时间线,用于「查看审批进度」。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `instanceId` | string | 是 | 流程实例 ID |
</G3ApiParams>
返回内容包含:
| 项 | 说明 |
|---|---|
| 节点列表 | 每个节点的状态、应签/已签人数、审批人快照 |
| 连线状态 | 哪些连线走过 |
| 时间线 | 动作历史(`wf_action`),按发生时间排列 |
| `timing` | 节点耗时 `elapsedMs` 与进出时间 |
> **超时判定在前端做**:后端只返回节点已耗时与节点配置的 `b_due_hours`,**不判断是否超期、也不算剩余时间**。
> 另外,任务超时被标记为 `expired` 后,会从待办列表移除,但**实例仍保持 `running`**(超时只标记任务,不改流程)。
### `POST` `/workflow/runtime/instance/by-business`
按业务单据查审批状态:业务详情页的状态条用。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `moduleId` | string | 是 | 模块编码 |
| `businessId` | string | 是 | 业务主键 |
</G3ApiParams>
返回:最近一次实例、**当前用户在该单据上的待办**、以及该模块是否已配置绑定。
> 即使模块**没有**配置绑定也会正常返回(提示未接入审批),不会报错——业务详情页可以无脑调用。
### `POST` `/workflow/runtime/task/detail`
任务详情:任务 + 实例 + 节点配置(含表单策略)。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | string | 是 | 待办任务 ID |
</G3ApiParams>
返回节点配置里的表单策略(哪些字段只读、哪些必填),审批页据此渲染表单。
## 常见错误
| 场景 | 表现 |
|---|---|
| 模块没有启用的绑定 | `1000`,`message` 形如「模块「x」的事件「submit」没有匹配到流程绑定」 |
| `event` 不合法 | `1000`,只接受 `submit` / `resubmit` / `change` |
| 任务已被他人处理 | `1000`,状态已不是 `pending` / `claimed` |
| 流程已结束 | `1000`,实例状态不是 `running` |
| 不是候选审批人却抢占 | `1000`,「你不是该任务的候选审批人,无法抢占」 |
| 缺少动作权限 | `1000`,「没有模块「x」的动作权限「y」,请联系管理员授权」 |
| 并发推进实例 | `1000`,「流程实例已被并发推进(版本不匹配),请刷新后重试」 |
| 重复请求(带幂等键) | **不是错误**:`code: 0` + `duplicated: true` |
@@ -0,0 +1,393 @@
---
title: 流程配置
description: 流程定义、版本草稿、发布校验与模块绑定
pageClass: g3-api
---
# 流程配置
配置侧接口,前缀 `/workflow/**`。
**读取走通用接口**(列表用 `v_wf_definition` / `v_wf_version` / `v_wf_binding` 视图,设计器读回草稿用 `/data/loaddata` 查 `wf_*` 表);**写入一律走这里**——发布校验、版本快照、「已发布不可改」这些约束必须在后端一个事务内完成,通用保存接口不参与。
> 运行侧(送审、审批动作、待办、抄送)见[流程运行](/api/workflow-runtime)。
## 流程定义
### `POST` `/workflow/definition/save`
新建或修改流程定义。**新建时同时落一条空草稿的 v1 版本**,保证设计器一进去就有可保存的目标。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `definitionId` | string | 是 | 流程编码。新建时校验:**字母开头**,仅字母/数字/下划线,≤ 50 字符 |
| `name` | string | 是 | 流程名称 |
| `i18n` | string | | 多语言资源键 |
| `bz` | string | | 备注 |
| `canuse` | bool | | 是否启用,默认 `true` |
</G3ApiParams>
<G3ApiPayload title="请求体">
```json
{
"definitionId": "expense_approval",
"name": "费用报销审批",
"i18n": "workflow.expense_approval",
"canuse": true
}
```
</G3ApiPayload>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "保存成功",
"data": {
"definitionId": "expense_approval",
"versionId": "expense_approval_v1",
"created": true
}
}
```
</G3ApiPayload>
- `created` 为 `true` 表示**新建**(同时建了 v1 草稿);`false` 表示改的是已存在的定义。
- 修改已存在的定义时,`versionId` 返回该定义**当前最新的草稿版本**(没有草稿则为 `null`)。
### `POST` `/workflow/definition/delete`
删除流程定义,连同它的版本、节点、连线、审批人规则与绑定**一起删**。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `definitionId` | string | 是 | 流程编码 |
</G3ApiParams>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "删除成功",
"data": { "versions": 2, "bindings": 1 }
}
```
</G3ApiPayload>
> **有未结束实例时会拒绝删除**:`1000`「该流程还有 N 个未结束的实例,不能删除」。
> 运行历史必须能追溯到版本,不能留下悬空引用。未结束指状态为 `running` / `returned` / `error`。
## 版本与草稿
### `POST` `/workflow/version/draft/save`
保存草稿:**全量替换**该版本的节点 / 连线 / 审批人规则(幂等)。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `definitionId` | string | 是 | 流程编码 |
| `versionId` | string | | 目标版本;**留空则取该定义的草稿,没有就新建** |
| `draft` | object | 是 | 草稿内容 `{ nodes, edges }`,见下 |
| `canvas` | object | | 画布布局(只存不参与执行) |
</G3ApiParams>
`draft` 的结构与前端设计器的领域模型一致:
<G3ApiPayload title="draft 结构">
```json
{
"nodes": [
{
"nodeKey": "start",
"nodeType": "start",
"name": "开始",
"config": {},
"actorRules": []
},
{
"nodeKey": "dept_manager",
"nodeType": "user_task",
"name": "部门负责人审批",
"config": {
"approvalMode": "any",
"rejectMode": "return_initiator",
"dueHours": 24,
"allowClaim": true,
"allowInitiator": false,
"excludePreviousActors": true
},
"actorRules": [
{ "actorType": "dept_manager", "fallbackType": "admin" }
]
}
],
"edges": [
{ "edgeKey": "e1", "from": "start", "to": "dept_manager" }
]
}
```
</G3ApiPayload>
**字段说明**:
| 位置 | 字段 | 说明 |
|---|---|---|
| node | `nodeKey` | 版本内稳定编码,≤ 50 字符,**不可跨版本改**(运行时连线状态按它映射) |
| node | `nodeType` | `start` / `end` / `user_task` / `cc_task` / `service_task` / `exclusive_gateway` / `parallel_gateway` |
| node | `config.approvalMode` | `single` / `any` / `all` / `serial` / `n_of_m` / `percentage`。**留空按 `single` 处理**;无法识别的取值也按 1 票通过 |
| node | `config.rejectMode` | `terminate` / `return_initiator` / `return_previous` |
| node | `config.dueHours` | 审批时限(小时);**不配则任务没有应处理时间,永不超时** |
| node | `config.requiredCount` / `requiredRatio` | 配合 `n_of_m` / `percentage` 用 |
| node | `config.allowInitiator` | **显式 `false`** 才排除发起人(缺省允许) |
| node | `config.excludeCreator` | 排除创建人(当前以发起人近似) |
| node | `config.excludePreviousActors` | 排除此前节点出现过的人 |
| node | `config.allowClaim` | 是否允许抢占待办 |
| edge | `condition` | 条件 AST;**留空 = 无条件(默认边)** |
| rule | `actorType` | `user` / `initiator` / `field_user` / `dept_manager` / `initiator_manager` / `position` / `org_role` |
| rule | `fallbackType` | 解析为空时的兜底:`admin` / `initiator` |
> **`condition` 必须是对象或留空**。给了值却不是对象会直接报错——把脏条件静默当成「无条件边」会让流程走错分支,所以发布期就拒绝。
> **`actorRules` 的数组顺序就是串行会签的先后顺序**。
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "草稿已保存",
"data": {
"versionId": "expense_approval_v1",
"versionNo": 1,
"issues": [],
"edgeKeyMap": {}
}
}
```
</G3ApiPayload>
- `issues` 是**保存时顺带做的校验提示**(不阻断保存)。
- `edgeKeyMap` 只在连线编码因与其它版本冲突而被换新 ID 时出现,前端据此把本地 `edgeKey` 对齐到库里。
> 已发布版本**一律拒绝**保存草稿(`1000`)——改流程要复制新版本。
### `POST` `/workflow/version/validate`
校验草稿。传 `draft` 校验内存草稿(设计器边改边校验),传 `versionId` 读库校验。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `draft` | object | 二选一 | 内存草稿 |
| `versionId` | string | 二选一 | 读库校验的版本 |
</G3ApiParams>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "操作成功",
"data": {
"issues": [
{ "level": "error", "message": "节点「部门负责人审批」没有配置审批人规则" },
{ "level": "warn", "message": "节点「财务复核」没有配置审批时限" }
],
"errorCount": 1,
"warnCount": 1
}
}
```
</G3ApiPayload>
### `POST` `/workflow/version/publish`
发布版本:校验必须**零 error**,然后计算配置摘要并锁定节点与连线。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `versionId` | string | 是 | 要发布的草稿版本 |
</G3ApiParams>
<G3ApiPayload title="发布成功" :code="0">
```json
{
"code": 0,
"message": "发布成功",
"data": {
"published": true,
"checksum": "3f2a...",
"definitionId": "expense_approval",
"issues": []
}
}
```
</G3ApiPayload>
<G3ApiPayload title="校验未通过(仍是 code 0)" :code="0">
```json
{
"code": 0,
"message": "发布校验未通过",
"data": {
"published": false,
"issues": [
{ "level": "error", "message": "流程缺少结束节点" }
]
}
}
```
</G3ApiPayload>
> **校验不通过不是错误**:HTTP 与 `code` 都是成功,要靠 `data.published` 判断。
> 这样调用方能**一次看到全部问题**,而不是改一个报一个。
### `POST` `/workflow/version/retire`
停用版本:`published` → `retired`,不再接受新实例;**已有实例继续用原版本跑完**。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `versionId` | string | 是 | 要停用的已发布版本 |
</G3ApiParams>
### `POST` `/workflow/version/copy`
以某个版本为模板复制出一条**新草稿**(版本号 = 当前最大值 + 1)。
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `versionId` | string | 是 | 源版本(可以是已发布版本) |
</G3ApiParams>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "已复制为新草稿",
"data": { "versionId": "expense_approval_v3", "versionNo": 3 }
}
```
</G3ApiPayload>
> **同一定义同时只允许一条草稿**。已有草稿时会报 `1000`「该流程已有草稿版本「x」,请先发布或删除它」——避免「改的是哪一条」说不清。
## 流程绑定
绑定决定「哪个模块的哪个业务事件走哪个流程版本」。
### `POST` `/workflow/binding/save`
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `moduleId` | string | 是 | 模块编码 |
| `event` | string | 是 | 业务事件:`submit` / `resubmit` / `change` |
| `versionId` | string | 是 | 目标版本,**必须是已发布版本** |
| `bindingId` | string | | 留空=新建,传值=修改 |
| `match` | object | | 匹配条件(条件 AST),决定业务上下文是否走这个流程 |
| `orgScope` | object | | 组织范围 |
| `statusField` | string | | 业务表上的**状态列名**(状态投影写这一列) |
| `statusMap` | object | | 流程状态 → 业务状态值的映射 |
| `priority` | int | | 优先级,默认 0,**小的先匹配** |
| `effectiveFrom` / `effectiveTo` | string | | 生效区间 |
| `canuse` | bool | | 是否启用,默认 `true` |
| `bz` | string | | 备注 |
</G3ApiParams>
<G3ApiPayload title="请求体">
```json
{
"moduleId": "v_b_othercompany",
"event": "submit",
"versionId": "expense_approval_v1",
"match": { "field": "b_amount", "operator": "gt", "value": 0 },
"statusField": "b_status",
"statusMap": { "approved": 2, "rejected": 3, "returned": 4 },
"priority": 10,
"canuse": true
}
```
</G3ApiPayload>
<G3ApiPayload title="响应" :code="0">
```json
{
"code": 0,
"message": "保存成功",
"data": { "bindingId": "1234567890123", "created": true }
}
```
</G3ApiPayload>
**几条硬规则**:
- **绑定只能指向已发布版本**(`1000`,提示当前版本状态)。
- **同一模块 + 同一事件下,不能有两条同优先级的启用绑定**(`1000`)——否则无法确定用哪个流程。冲突时调整 `priority` 或停用其中一条。
- **保存绑定会顺带备好该模块的动作权限点**:否则管理员在用户权限页看不到可授权的权限点,非管理员送审会被白名单拒绝。这一步是幂等的,不会覆盖管理员改过的名称。
> `match` 留空表示**该模块该事件一律走这个流程**。`statusMap` 决定审批结果如何写回业务表状态列;不配 `statusField` 则跳过状态投影(只读模块的正常情况)。
### `POST` `/workflow/binding/delete`
<G3ApiParams title="请求字段">
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `bindingId` | string | 是 | 绑定 ID |
</G3ApiParams>
## 常见错误
| 场景 | 表现 |
|---|---|
| 流程编码非法 | `1000`「流程编码只能以字母开头,由字母、数字、下划线组成,且不超过 50 个字符」 |
| 版本不存在 | `1000`「版本「x」不存在」 |
| 非草稿版本发布 | `1000`「只有草稿版本可以发布,当前版本状态:published」 |
| 非已发布版本停用 | `1000`「只有已发布版本可以停用,当前版本状态:draft」 |
| 绑定指向非已发布版本 | `1000`「绑定只能指向已发布版本,当前版本状态:draft」 |
| 同优先级启用绑定冲突 | `1000`「同一模块同一事件下已存在同优先级的启用绑定「x」」 |
| 删除有未结束实例的定义 | `1000`「该流程还有 N 个未结束的实例,不能删除」 |
| 连线/审批人规则条件不是对象 | `1000`,指明是「审批人规则」还是「连线「x」」的条件 |