871 lines
23 KiB
Markdown
871 lines
23 KiB
Markdown
# FMS 后端 API 需求规范
|
||
|
||
## 1. 设计定位
|
||
|
||
FMS 是面向内部用户的多机构 ERP 系统。
|
||
|
||
当前阶段只确定以下基础原则:
|
||
|
||
- 所有 API 请求统一使用 `POST`。
|
||
- 前端登录时通过请求头传递机构码。
|
||
- 一个机构对应一个独立数据库。
|
||
- 登录成功后机构码绑定到访问令牌,后续请求由后端从令牌获取机构码并选择数据库。
|
||
- 前端不接触数据库连接信息。
|
||
- 后端负责身份认证、机构数据库路由和统一响应。
|
||
- 权限编码只用于前端禁用按钮,暂不作为后端业务接口的授权条件。
|
||
- 普通查询和保存采用结构化数据协议,不要求前端编写参数占位符。
|
||
- 高级只读查询可以通过独立的 `load_sql` 接口提交 SQL。
|
||
- 新业务在现有数据库表和视图能够支持的情况下,只需开发前端页面,不需要增加后端业务接口。
|
||
|
||
当前提供以下接口:
|
||
|
||
| 方法 | 路径 | 用途 | 是否需要登录 |
|
||
|------|------|------|--------------|
|
||
| `POST` | `/api/auth/login` | 登录并获取用户及按钮权限 | 否 |
|
||
| `POST` | `/api/data/load` | 通用结构化数据查询 | 是 |
|
||
| `POST` | `/api/data/load_sql` | 高级只读 SQL 查询 | 是 |
|
||
| `POST` | `/api/data/save` | 通用数据保存 | 是 |
|
||
|
||
## 2. 通用请求规范
|
||
|
||
### 2.1 HTTP 方法
|
||
|
||
- 所有接口只使用 `POST`。
|
||
- 不使用 `GET`、`PUT`、`PATCH` 或 `DELETE`。
|
||
- 查询、创建、修改、删除和业务操作都通过后续定义的 `POST` 接口完成。
|
||
|
||
### 2.2 数据格式
|
||
|
||
- 请求和响应使用 `application/json; charset=utf-8`。
|
||
- 除登录路由使用的 `orgid` 请求头外,请求参数放在 JSON 请求体中。
|
||
- 需要登录的接口通过 Bearer Token 认证。
|
||
- 响应不能包含数据库连接信息、令牌密钥、密码或服务端调用栈。
|
||
|
||
需要登录的请求必须包含:
|
||
|
||
```http
|
||
Authorization: Bearer <access_token>
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
### 2.3 命名规范
|
||
|
||
- API 路径、请求字段和响应字段优先使用简短、明确的全小写名称。
|
||
- 能使用单个单词或常用组合词时,不使用驼峰命名法,例如 `username`、`orgid`。
|
||
- 多个单词无法简洁表达时使用 `snake_case`,例如 `access_token`、`display_name`。
|
||
- JSON 字段不使用 `camelCase`,例如不使用 `accessToken`、`displayName`。
|
||
- TypeScript 接口和类型名称遵循语言惯例使用 `PascalCase`,其字段名与 API JSON 字段保持一致。
|
||
|
||
### 2.4 机构标识
|
||
|
||
机构标识统一命名为 `orgid`。
|
||
|
||
规则:
|
||
|
||
- `orgid` 类型为非空字符串。
|
||
- 登录请求必须通过 `orgid` 请求头传递机构标识。
|
||
- 登录成功后,`orgid` 保存在访问令牌声明或服务端会话中。
|
||
- 后续业务请求不在请求头或请求体中重复传递 `orgid`。
|
||
- 后端从访问令牌中获取 `orgid` 并选择机构数据库。
|
||
- 前端只传机构码,不能传数据库名称、地址、端口、账号或密码。
|
||
- 机构码必须作为完整字段值传递,不能用于拼接数据库连接字符串或 SQL。
|
||
- 机构码不存在、已停用或未配置数据库时,请求失败。
|
||
|
||
登录请求头示例:
|
||
|
||
```http
|
||
orgid: SH001
|
||
```
|
||
|
||
### 2.5 通用成功响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
### 2.6 通用失败响应
|
||
|
||
```json
|
||
{
|
||
"code": 1201,
|
||
"message": "organization not found",
|
||
"details": {
|
||
"orgid": "SH001"
|
||
}
|
||
}
|
||
```
|
||
|
||
约定:
|
||
|
||
- `code = 0` 表示成功。
|
||
- HTTP 状态码表示错误类别,`code` 表示具体错误。
|
||
- `data` 只在成功且接口有返回数据时出现。
|
||
- `details` 为可选的结构化错误信息。
|
||
|
||
## 3. 机构数据库路由
|
||
|
||
### 3.1 数据隔离模型
|
||
|
||
FMS 采用一个机构一个数据库的隔离方式:
|
||
|
||
```text
|
||
orgid: SH001 -> database: fms_sh001
|
||
orgid: BJ001 -> database: fms_bj001
|
||
orgid: GZ001 -> database: fms_gz001
|
||
```
|
||
|
||
上述映射只表示逻辑关系,真实数据库名称和连接信息不能返回给前端。
|
||
|
||
### 3.2 后端处理流程
|
||
|
||
后端收到请求后按以下顺序处理:
|
||
|
||
1. 登录时从 `orgid` 请求头获取机构标识;后续请求从访问令牌获取机构标识。
|
||
2. 校验 `orgid` 的格式。
|
||
3. 从服务端机构配置中查找机构。
|
||
4. 检查机构是否启用以及数据库配置是否可用。
|
||
5. 获取该机构对应的数据库连接或连接池。
|
||
6. 在该机构数据库中完成认证或后续业务操作。
|
||
7. 返回统一响应,不暴露数据库连接信息。
|
||
|
||
### 3.3 登录状态与机构绑定
|
||
|
||
- 登录成功后,访问令牌必须绑定当前 `orgid` 和用户标识。
|
||
- 后续请求只以访问令牌绑定的 `orgid` 为准。
|
||
- 后续请求不能通过请求头或请求体覆盖令牌中的 `orgid`。
|
||
- 用户需要切换机构时,必须在登录请求头中传入目标 `orgid` 并重新登录。
|
||
- 当前不提供 refresh;访问令牌过期后需要重新登录。
|
||
- 当前不提供 logout;客户端退出时删除本地访问令牌。
|
||
|
||
### 3.4 连接管理
|
||
|
||
- 数据库连接配置由后端集中管理。
|
||
- 后端可以为每个机构维护独立连接池。
|
||
- 同一次请求只能访问一个机构数据库。
|
||
- 机构数据库不可用时,不能自动切换到其他机构数据库。
|
||
- 日志可以记录机构码,但不能记录数据库密码或完整连接字符串。
|
||
|
||
## 4. 登录接口
|
||
|
||
### 4.1 登录页面
|
||
|
||
登录页面必须提供以下输入项:
|
||
|
||
| 输入项 | 必填 | 说明 |
|
||
|--------|------|------|
|
||
| 机构码 | 是 | 用户所属机构的 `orgid` |
|
||
| 用户名 | 是 | 用户登录账号 |
|
||
| 密码 | 是 | 用户登录密码 |
|
||
|
||
用户点击登录后,前端将机构码放入 `orgid` 请求头,将用户名和密码放入 JSON 请求体。
|
||
|
||
### 4.2 请求
|
||
|
||
```http
|
||
POST /api/auth/login
|
||
orgid: SH001
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"username": "zhangsan",
|
||
"password": "password"
|
||
}
|
||
```
|
||
|
||
| 字段 | 位置 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `orgid` | header | string | 是 | 登录页面填写的机构码,用于选择机构数据库 |
|
||
| `username` | body | string | 是 | 登录账号 |
|
||
| `password` | body | string | 是 | 登录密码 |
|
||
|
||
### 4.3 处理规则
|
||
|
||
- 后端先根据 `orgid` 选择机构数据库,再验证用户名和密码。
|
||
- 登录失败时不能说明账号是否存在。
|
||
- 密码不能写入日志或错误响应。
|
||
- 登录成功后令牌绑定机构码和用户标识。
|
||
- 当前不签发 refresh token。
|
||
|
||
### 4.4 成功响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"access_token": "access-token",
|
||
"token_type": "Bearer",
|
||
"expires_in": 1800,
|
||
"organization": {
|
||
"orgid": "SH001",
|
||
"name": "上海分公司"
|
||
},
|
||
"user": {
|
||
"id": 1001,
|
||
"username": "zhangsan",
|
||
"display_name": "张三"
|
||
},
|
||
"permissions": [
|
||
"sales.order.create",
|
||
"sales.order.approve",
|
||
"finance.payment.view"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 前端按钮权限
|
||
|
||
- `permissions` 是当前登录用户拥有的权限编码数组。
|
||
- 权限编码由机构数据库中的用户、角色和权限配置产生。
|
||
- 权限编码建议使用 `<模块>.<资源>.<动作>` 格式。
|
||
- 前端根据权限编码设置按钮的禁用状态。
|
||
- 用户没有对应权限时,按钮保持可见但不可操作。
|
||
- 当前权限只控制前端交互,不作为后端业务授权条件。
|
||
|
||
前端判断示例:
|
||
|
||
```typescript
|
||
const canApprove = permissions.includes('sales.order.approve')
|
||
```
|
||
|
||
## 6. 通用加载接口
|
||
|
||
### 6.1 设计原则
|
||
|
||
- 前端提交数据源、返回字段、过滤条件、排序和分页。
|
||
- 前端不提交 SQL,也不编写参数占位符。
|
||
- 后端根据结构化请求生成查询 SQL,并在后端内部绑定所有数据值。
|
||
- 查询数据源可以是数据库表或视图。
|
||
- 复杂关联、聚合和计算字段优先封装为数据库视图,再通过本接口查询。
|
||
- 后端不为具体业务定义查询接口。
|
||
|
||
### 6.2 请求
|
||
|
||
```http
|
||
POST /api/data/load
|
||
Authorization: Bearer <access_token>
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"source": "v_sales_order_list",
|
||
"fields": [
|
||
"id",
|
||
"order_no",
|
||
"customer_name",
|
||
"status",
|
||
"total_amount",
|
||
"created_at"
|
||
],
|
||
"filter": {
|
||
"logic": "and",
|
||
"items": [
|
||
{
|
||
"field": "status",
|
||
"op": "eq",
|
||
"value": "pending"
|
||
},
|
||
{
|
||
"logic": "or",
|
||
"items": [
|
||
{
|
||
"field": "customer_name",
|
||
"op": "contains",
|
||
"value": "海运"
|
||
},
|
||
{
|
||
"field": "order_no",
|
||
"op": "starts_with",
|
||
"value": "SO2026"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
"sort": [
|
||
{
|
||
"field": "created_at",
|
||
"direction": "desc"
|
||
}
|
||
],
|
||
"page": {
|
||
"page_no": 1,
|
||
"page_size": 20
|
||
},
|
||
"include_total": true
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `source` | string | 是 | 数据库表名或视图名 |
|
||
| `fields` | string[] | 是 | 返回字段,必须是非空数组 |
|
||
| `filter` | object | 否 | 过滤条件组 |
|
||
| `sort` | array | 否 | 排序规则 |
|
||
| `page` | object | 否 | 分页;省略时使用默认分页 |
|
||
| `include_total` | boolean | 否 | 是否计算符合条件的总记录数,默认 `false` |
|
||
|
||
### 6.3 标识符规则
|
||
|
||
- `source`、字段名和排序字段只能是数据库标识符,不能包含 SQL 表达式。
|
||
- 标识符必须符合 `^[a-z_][a-z0-9_]*$`。
|
||
- `source` 可以包含一个 schema 前缀,例如 `public.v_sales_order_list`。
|
||
- 带 schema 的数据源必须符合 `<schema>.<object>` 格式。
|
||
- 后端必须正确引用标识符,不能把标识符当作数据值拼接。
|
||
- 字段必须真实存在于指定数据源中。
|
||
- `fields` 只返回前端明确请求的字段,不支持 `*`。
|
||
|
||
### 6.4 过滤条件
|
||
|
||
过滤条件分为条件项和条件组。
|
||
|
||
条件项:
|
||
|
||
```json
|
||
{
|
||
"field": "status",
|
||
"op": "eq",
|
||
"value": "pending"
|
||
}
|
||
```
|
||
|
||
条件组:
|
||
|
||
```json
|
||
{
|
||
"logic": "and",
|
||
"items": [
|
||
{
|
||
"field": "status",
|
||
"op": "eq",
|
||
"value": "pending"
|
||
},
|
||
{
|
||
"field": "total_amount",
|
||
"op": "gte",
|
||
"value": 1000
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
条件组规则:
|
||
|
||
- `logic` 只允许 `and` 或 `or`。
|
||
- `items` 必须是非空数组。
|
||
- 条件组可以嵌套条件组。
|
||
- 所有 `value` 都由后端内部参数绑定,不作为 SQL 文本执行。
|
||
|
||
支持的操作符:
|
||
|
||
| `op` | 说明 | `value` 规则 |
|
||
|------|------|--------------|
|
||
| `eq` | 等于 | 单个值 |
|
||
| `ne` | 不等于 | 单个值 |
|
||
| `gt` | 大于 | 单个值 |
|
||
| `gte` | 大于或等于 | 单个值 |
|
||
| `lt` | 小于 | 单个值 |
|
||
| `lte` | 小于或等于 | 单个值 |
|
||
| `in` | 在列表中 | 非空数组 |
|
||
| `not_in` | 不在列表中 | 非空数组 |
|
||
| `between` | 在范围内,包含边界 | 两个元素的数组 |
|
||
| `contains` | 文本包含 | string |
|
||
| `starts_with` | 文本开头匹配 | string |
|
||
| `ends_with` | 文本结尾匹配 | string |
|
||
| `is_null` | 为 `NULL` | 不传 `value` |
|
||
| `is_not_null` | 不为 `NULL` | 不传 `value` |
|
||
|
||
补充规则:
|
||
|
||
- `eq` 的值为 `null` 时按 `is_null` 处理。
|
||
- `ne` 的值为 `null` 时按 `is_not_null` 处理。
|
||
- `contains`、`starts_with` 和 `ends_with` 中的 `%`、`_` 默认作为普通字符处理。
|
||
- 空数组不能用于 `in`、`not_in` 或 `between`。
|
||
|
||
### 6.5 排序和分页
|
||
|
||
- `direction` 只允许 `asc` 或 `desc`。
|
||
- 可以指定多个排序字段,后端按数组顺序生成排序规则。
|
||
- `page_no` 从 1 开始,默认值为 1。
|
||
- `page_size` 默认值为 20,最大值为 5000。
|
||
- 前端需要稳定分页时,应包含主键或其他唯一字段作为最后一个排序字段。
|
||
|
||
### 6.6 成功响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"rows": [
|
||
{
|
||
"id": 128,
|
||
"order_no": "SO20260001",
|
||
"customer_name": "长荣海运",
|
||
"status": "pending",
|
||
"total_amount": 3500,
|
||
"created_at": "2026-07-14T10:30:00+08:00"
|
||
}
|
||
],
|
||
"row_count": 1,
|
||
"total": 36,
|
||
"page_no": 1,
|
||
"page_size": 20
|
||
}
|
||
}
|
||
```
|
||
|
||
响应规则:
|
||
|
||
- `rows` 始终是数组,没有记录时返回空数组。
|
||
- `row_count` 是本次实际返回的记录数。
|
||
- `total` 只在 `include_total = true` 时返回,否则为 `null`。
|
||
- `total` 表示忽略分页后符合过滤条件的记录数。
|
||
|
||
## 7. 高级 SQL 加载接口
|
||
|
||
### 7.1 适用范围
|
||
|
||
`/api/data/load_sql` 用于结构化 `/api/data/load` 无法表达的高级只读查询,例如复杂关联、CTE、窗口函数和聚合查询。
|
||
|
||
规则:
|
||
|
||
- 该接口是可选的高级能力。
|
||
- 只允许执行单条只读 SQL。
|
||
- 允许 `SELECT`,以及所有 CTE 都是只读查询的 `WITH`。
|
||
- 不允许 `INSERT`、`UPDATE`、`DELETE`、`MERGE`、DDL、权限管理和事务控制语句。
|
||
- 不允许通过分号附加第二条语句。
|
||
- 即使最外层是 `SELECT`,也不允许在 CTE 中写入数据。
|
||
- SQL 不支持参数占位符,前端不能把最终用户输入直接拼接到 SQL 中。
|
||
- 包含页面动态筛选值的查询应优先使用 `/api/data/load`。
|
||
- 返回行数和执行超时遵循普通加载接口限制。
|
||
|
||
### 7.2 请求
|
||
|
||
```http
|
||
POST /api/data/load_sql
|
||
Authorization: Bearer <access_token>
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"sql": "SELECT customer_id, customer_name, COUNT(*) AS order_count, SUM(total_amount) AS total_amount FROM v_sales_order_list GROUP BY customer_id, customer_name ORDER BY total_amount DESC LIMIT 100"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `sql` | string | 是 | 单条只读 SQL |
|
||
|
||
### 7.3 成功响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"rows": [
|
||
{
|
||
"customer_id": 42,
|
||
"customer_name": "长荣海运",
|
||
"order_count": 18,
|
||
"total_amount": 63000
|
||
}
|
||
],
|
||
"row_count": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
响应规则:
|
||
|
||
- `rows` 始终是数组,没有记录时返回空数组。
|
||
- `row_count` 是本次实际返回的记录数。
|
||
- 排序、分页和总数由 SQL 自身决定。
|
||
|
||
## 8. 通用保存接口
|
||
|
||
### 8.1 设计原则
|
||
|
||
- 保存接口支持在一次请求中新增、修改或删除多条数据。
|
||
- 前端提交目标表、操作类型、数据值和过滤条件。
|
||
- 前端不提交 SQL,也不编写参数占位符。
|
||
- 后端根据结构化请求生成 SQL,并在后端内部绑定所有数据值。
|
||
- 一个请求中的所有变更使用同一个数据库事务。
|
||
- 任一变更失败时,整个请求回滚。
|
||
- 后端不为具体业务定义保存接口。
|
||
|
||
### 8.2 请求
|
||
|
||
```http
|
||
POST /api/data/save
|
||
Authorization: Bearer <access_token>
|
||
Content-Type: application/json; charset=utf-8
|
||
```
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"changes": [
|
||
{
|
||
"action": "insert",
|
||
"target": "sales_order",
|
||
"values": {
|
||
"id": "019f62d5-4115-7b22-8c51-8f89e2e8b421",
|
||
"order_no": "SO20260001",
|
||
"customer_id": 42,
|
||
"status": "draft",
|
||
"version": 1
|
||
},
|
||
"returning": [
|
||
"id",
|
||
"order_no"
|
||
],
|
||
"expect_rows": 1
|
||
},
|
||
{
|
||
"action": "insert",
|
||
"target": "sales_order_item",
|
||
"values": {
|
||
"id": "019f62d5-4115-79db-ab88-99e8c5dc7f21",
|
||
"order_id": "019f62d5-4115-7b22-8c51-8f89e2e8b421",
|
||
"product_id": 301,
|
||
"quantity": 2,
|
||
"unit_price": 1750
|
||
},
|
||
"returning": [
|
||
"id"
|
||
],
|
||
"expect_rows": 1
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `changes` | array | 是 | 按顺序执行的变更,必须是非空数组 |
|
||
|
||
### 8.3 Change 结构
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `action` | string | 是 | `insert`、`update` 或 `delete` |
|
||
| `target` | string | 是 | 目标表或可写视图 |
|
||
| `values` | object | insert、update 必填 | 要写入的字段和值 |
|
||
| `filter` | object | update、delete 必填 | 确定修改或删除范围的过滤条件 |
|
||
| `returning` | string[] | 否 | 保存后需要返回的字段 |
|
||
| `expect_rows` | integer | 否 | 实际影响行数必须等于该非负整数 |
|
||
|
||
### 8.4 保存规则
|
||
|
||
- `target` 和字段名遵循查询接口的标识符规则。
|
||
- `values` 必须是非空对象。
|
||
- `values` 的值可以是 string、number、boolean、null、array 或 object。
|
||
- array 和 object 作为数据库字段值时,由数据库字段类型决定如何转换。
|
||
- `update` 和 `delete` 必须提供非空 `filter`,不允许无条件修改或删除整表数据。
|
||
- 保存过滤条件使用查询接口定义的条件结构和操作符。
|
||
- `returning` 省略时返回空数组。
|
||
- `expect_rows` 不匹配时整个请求失败并回滚。
|
||
- 数据库默认值通过省略对应字段触发。
|
||
- `values` 不支持 SQL 函数、字段引用或其他原始 SQL 表达式。
|
||
|
||
### 8.5 多表保存和主键
|
||
|
||
保存接口不提供步骤结果占位符,也不支持后续变更引用前序变更的返回值。
|
||
|
||
多表新增时,前端应提前生成 UUID 主键,并把主键和外键直接写入各个 change 的 `values`:
|
||
|
||
```text
|
||
前端生成订单 id
|
||
-> 前端把订单 id 写入订单数据
|
||
-> 前端把同一个订单 id 写入明细的 order_id
|
||
-> 一次提交所有 changes
|
||
-> 后端在同一事务中保存
|
||
```
|
||
|
||
如果某个表必须使用数据库生成的主键,则只能在本次请求的 `returning` 中获取,不能在同一次保存请求的后续 change 中引用。需要多表原子保存时,应优先使用前端生成的 UUID 主键。
|
||
|
||
### 8.6 修改和乐观锁示例
|
||
|
||
```json
|
||
{
|
||
"changes": [
|
||
{
|
||
"action": "update",
|
||
"target": "sales_order",
|
||
"values": {
|
||
"status": "approved",
|
||
"version": 4
|
||
},
|
||
"filter": {
|
||
"logic": "and",
|
||
"items": [
|
||
{
|
||
"field": "id",
|
||
"op": "eq",
|
||
"value": "019f62d5-4115-7b22-8c51-8f89e2e8b421"
|
||
},
|
||
{
|
||
"field": "version",
|
||
"op": "eq",
|
||
"value": 3
|
||
}
|
||
]
|
||
},
|
||
"returning": [
|
||
"id",
|
||
"status",
|
||
"version"
|
||
],
|
||
"expect_rows": 1
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
前端读取当前版本号,保存时同时提交旧版本条件和新版本值。当其他用户已经修改记录时,`expect_rows` 检查失败,整个请求回滚。
|
||
|
||
### 8.7 成功响应
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"results": [
|
||
{
|
||
"index": 0,
|
||
"action": "insert",
|
||
"affected_rows": 1,
|
||
"rows": [
|
||
{
|
||
"id": "019f62d5-4115-7b22-8c51-8f89e2e8b421",
|
||
"order_no": "SO20260001"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"index": 1,
|
||
"action": "insert",
|
||
"affected_rows": 1,
|
||
"rows": [
|
||
{
|
||
"id": "019f62d5-4115-79db-ab88-99e8c5dc7f21"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
响应规则:
|
||
|
||
- `results` 顺序与请求中的 `changes` 一致。
|
||
- `rows` 始终是数组。
|
||
- `affected_rows` 表示该变更实际影响的记录数。
|
||
- 事务提交成功后才返回成功响应。
|
||
|
||
## 9. 错误规范
|
||
|
||
| HTTP 状态码 | code | 说明 |
|
||
|-------------|------|------|
|
||
| 400 | 1001 | 请求 JSON 或字段结构错误 |
|
||
| 400 | 1006 | 请求大小超限 |
|
||
| 401 | 1101 | 机构码、用户名或密码错误 |
|
||
| 401 | 1102 | 访问令牌无效或已过期 |
|
||
| 404 | 1201 | 机构不存在 |
|
||
| 409 | 1202 | 机构已停用 |
|
||
| 500 | 1203 | 机构数据库未配置或配置无效 |
|
||
| 400 | 1301 | 数据源、目标或字段标识符格式错误 |
|
||
| 400 | 1302 | 过滤条件、排序或分页格式错误 |
|
||
| 400 | 1303 | 保存变更结构错误 |
|
||
| 400 | 1304 | SQL 为空、包含多条语句或不是只读查询 |
|
||
| 409 | 2001 | `expect_rows` 与实际影响行数不一致 |
|
||
| 409 | 2002 | 唯一键、外键、非空或检查约束冲突 |
|
||
| 409 | 2003 | 死锁、序列化失败或其他并发冲突 |
|
||
| 422 | 3001 | 数据源、目标或字段不存在 |
|
||
| 422 | 3002 | 数据值无法转换为数据库字段类型 |
|
||
| 500 | 3003 | 数据库执行失败 |
|
||
| 503 | 9001 | 机构数据库不可用 |
|
||
| 500 | 9002 | 服务端内部错误 |
|
||
| 504 | 9003 | 查询或保存执行超时 |
|
||
|
||
错误响应不能泄露真实数据库名称、地址、端口、账号、密码或连接字符串。
|
||
|
||
保存请求执行失败时,错误响应的 `details` 应包含失败的 `change_index`。约束冲突可以返回约束名称,但不能返回生成后的完整 SQL。
|
||
|
||
## 10. 执行限制
|
||
|
||
默认限制:
|
||
|
||
| 项目 | 限制 |
|
||
|------|------|
|
||
| 单次请求大小 | 2 MB |
|
||
| load 最大返回行数 | 5000 行 |
|
||
| load_sql 最大返回行数 | 5000 行 |
|
||
| load 最大过滤条件数 | 200 个 |
|
||
| save 最大变更数 | 100 个 |
|
||
| 单个 change 最大字段数 | 500 个 |
|
||
| load 执行超时 | 30 秒 |
|
||
| load_sql 执行超时 | 30 秒 |
|
||
| save 执行超时 | 60 秒 |
|
||
|
||
规则:
|
||
|
||
- 超过返回行数限制时请求失败,不静默截断结果。
|
||
- save 超时包含全部变更和事务提交过程。
|
||
- 限制值可以通过后端配置调整。
|
||
|
||
## 11. 当前范围
|
||
|
||
当前文档只定义:
|
||
|
||
- 全部请求使用 `POST` 的通用规范。
|
||
- 机构码传递和机构数据库路由规则。
|
||
- 登录接口。
|
||
- 登录响应中的用户信息和前端按钮权限。
|
||
- 通用结构化查询接口。
|
||
- 高级只读 SQL 查询接口。
|
||
- 通用结构化保存接口。
|
||
- 通用响应和基础错误码。
|
||
|
||
当前不提供原有的 `/api/query` 和 `/api/transaction`。数据访问统一使用 `/api/data/load`、`/api/data/load_sql` 和 `/api/data/save`。
|
||
|
||
文件、导入、导出、报表以及页面和菜单加载方式将在后续单独设计。
|
||
|
||
## 12. TypeScript 类型参考
|
||
|
||
```typescript
|
||
interface ApiResponse<T> {
|
||
code: number
|
||
message: string
|
||
data?: T
|
||
details?: Record<string, unknown>
|
||
}
|
||
|
||
interface LoginRequest {
|
||
username: string
|
||
password: string
|
||
}
|
||
|
||
interface LoginResult {
|
||
access_token: string
|
||
token_type: 'Bearer'
|
||
expires_in: number
|
||
organization: {
|
||
orgid: string
|
||
name: string
|
||
}
|
||
user: {
|
||
id: string | number
|
||
username: string
|
||
display_name: string
|
||
}
|
||
permissions: string[]
|
||
}
|
||
|
||
type DataScalar = string | number | boolean | null
|
||
type DataValue = DataScalar | DataValue[] | { [key: string]: DataValue }
|
||
|
||
type FilterNode = FilterCondition | FilterGroup
|
||
|
||
interface FilterCondition {
|
||
field: string
|
||
op:
|
||
| 'eq'
|
||
| 'ne'
|
||
| 'gt'
|
||
| 'gte'
|
||
| 'lt'
|
||
| 'lte'
|
||
| 'in'
|
||
| 'not_in'
|
||
| 'between'
|
||
| 'contains'
|
||
| 'starts_with'
|
||
| 'ends_with'
|
||
| 'is_null'
|
||
| 'is_not_null'
|
||
value?: DataValue
|
||
}
|
||
|
||
interface FilterGroup {
|
||
logic: 'and' | 'or'
|
||
items: FilterNode[]
|
||
}
|
||
|
||
interface LoadRequest {
|
||
source: string
|
||
fields: string[]
|
||
filter?: FilterGroup
|
||
sort?: Array<{
|
||
field: string
|
||
direction: 'asc' | 'desc'
|
||
}>
|
||
page?: {
|
||
page_no: number
|
||
page_size: number
|
||
}
|
||
include_total?: boolean
|
||
}
|
||
|
||
interface LoadResult<T = Record<string, unknown>> {
|
||
rows: T[]
|
||
row_count: number
|
||
total: number | null
|
||
page_no: number
|
||
page_size: number
|
||
}
|
||
|
||
interface LoadSqlRequest {
|
||
sql: string
|
||
}
|
||
|
||
interface LoadSqlResult<T = Record<string, unknown>> {
|
||
rows: T[]
|
||
row_count: number
|
||
}
|
||
|
||
type SaveAction = 'insert' | 'update' | 'delete'
|
||
|
||
interface SaveChange {
|
||
action: SaveAction
|
||
target: string
|
||
values?: Record<string, DataValue>
|
||
filter?: FilterGroup
|
||
returning?: string[]
|
||
expect_rows?: number
|
||
}
|
||
|
||
interface SaveRequest {
|
||
changes: SaveChange[]
|
||
}
|
||
|
||
interface SaveResult<T = Record<string, unknown>> {
|
||
results: Array<{
|
||
index: number
|
||
action: SaveAction
|
||
affected_rows: number
|
||
rows: T[]
|
||
}>
|
||
}
|
||
```
|