214 lines
6.7 KiB
Markdown
214 lines
6.7 KiB
Markdown
---
|
||
title: 数据写入
|
||
description: saveobjt / delete,含 $original、$expr 与 guards
|
||
pageClass: g3-api
|
||
---
|
||
|
||
# 数据写入
|
||
|
||
## `POST` `/data/saveobjt`
|
||
|
||
请求体是一个**数组**:一次可提交多个表的增、改、删,**全部在同一个事务里** —— 任何一项失败整批回滚,不会留下半套数据。
|
||
|
||
### 请求字段
|
||
|
||
<G3ApiParams title="请求字段">
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `table` | string | 是 | 保存目标表 |
|
||
| `key_field` | string | 是 | 主键列名,用于定位要改/删的行 |
|
||
| `updates` | object[] | | 要更新的行,主键值必须在行里 |
|
||
| `inserts` | object[] | | 要新增的行 |
|
||
| `deletes` | object[] | | 要删除的行(用 `key_field` 定位) |
|
||
| `delete_target` | string | | 声明 `deletes` 走哪条路径:`module`(按模块配置联动删子表,需 `moduleCode`)或 `direct`(直接删,需 `table` / `key_field`) |
|
||
| `guards` | object | | 前置条件,见下 |
|
||
|
||
</G3ApiParams>
|
||
|
||
<G3ApiPayload title="请求体">
|
||
|
||
```json
|
||
[
|
||
{
|
||
"table": "b_othercompany",
|
||
"key_field": "b_id",
|
||
"updates": [{ "b_id": "C001", "b_name": "新名字" }],
|
||
"inserts": [{ "b_id": "C002", "b_name": "新公司" }],
|
||
"deletes": [{ "b_id": "C003" }]
|
||
}
|
||
]
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
成功时 `data` 为 `null`,`message` 为 `保存成功`:
|
||
|
||
<G3ApiPayload title="响应" :code="0">
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "保存成功",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
> **没传 `delete_target` 时,`deletes` 是逐行按主键直删**,不会触发模块配置里的联动删除。需要联动就把 `delete_target` 声明成 `module` 并带上 `moduleCode`。
|
||
|
||
### 行内的两个保留键
|
||
|
||
键名以 `$` 开头的不会被当成业务列:
|
||
|
||
<G3ApiParams title="行内保留键">
|
||
|
||
| 键 | 用途 | 说明 |
|
||
|---|---|---|
|
||
| `$original` | 乐观锁:声明「我改动之前看到的值」 | 这些字段**只进 `WHERE` 不进 `SET`**;对不上就是 `2003`。值为 `null` 时生成 `IS NULL` |
|
||
| `$expr` | 字段自增:`{"$expr": "b_amount + 5"}` | 只支持「本表列 ± 数值常量」,列必须是数值类型 |
|
||
|
||
</G3ApiParams>
|
||
|
||
<G3ApiPayload title="行内示例">
|
||
|
||
```json
|
||
{
|
||
"b_id": "C001",
|
||
"b_amount": { "$expr": "b_amount + 5" },
|
||
"$original": { "b_name": "旧名字", "b_row_version": 3 }
|
||
}
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
- `$original` 里带 `b_row_version` 时走**整行版本锁**:服务端校验通过后自动 `+1`,请求里同名的普通值不生效(避免 `SET` 里出现两次该列)。
|
||
- `inserts` 的行**不允许**带 `$original` / `$expr` —— 新增的行没有原值可比,带了会直接报 `1000`。
|
||
- `$expr` 的字段不存在、不可写、不是数值类型、格式不对,都会在**任何一行写入之前**报 `1000`(这类参数错误不会等到写到第 N 行才暴露)。
|
||
|
||
### guards:写入前置条件
|
||
|
||
让「判断」和「写入」发生在**同一条 SQL** 里,避免先查后写的竞态。
|
||
|
||
<G3ApiPayload title="带 guards 的请求体">
|
||
|
||
```json
|
||
{
|
||
"table": "b_othercompany",
|
||
"key_field": "b_id",
|
||
"updates": [{ "b_id": "C001", "b_name": "新名字" }],
|
||
"guards": {
|
||
"updates": [
|
||
{
|
||
"condition": "{b_state} = 'draft'",
|
||
"refs": ["b_othercompany"],
|
||
"message": "只有草稿状态才能改名",
|
||
"retryable": false
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
`guards` 下的三个分节键按动作区分 —— `inserts` / `updates` / **`deletes`**(注意是 `deletes`):
|
||
|
||
<G3ApiParams title="guards 字段">
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `condition` | string | 是 | SQL 布尔表达式;`{列名}` 是受控占位符,替换为 `t.[列名]`;**不允许注释** |
|
||
| `refs` | string[] | | 声明引用的表,只进日志 |
|
||
| `message` | string | | 条件不成立时给用户看的文案 |
|
||
| `retryable` | boolean | | 默认 `true`;`false` 时前端文案不追加「请刷新后重试」 |
|
||
|
||
</G3ApiParams>
|
||
|
||
条件不成立时返回 `2002`,`message` 就是上面写的 `message`,`data` 带 `table` / `action` / `index`。
|
||
|
||
两条互斥规则(同时用会报 `1000`):
|
||
|
||
- `guards.deletes` 与 `delete_target` 互斥;
|
||
- `guards.deletes` 与 `dependent_deletes` 互斥。
|
||
|
||
原因是带条件的删除只能逐行唯一定位,而 `delete_target` / 依赖删除那两条旁路拿不到 guards —— 与其让条件被静默忽略,不如直接拒绝。
|
||
|
||
### 三个错误码怎么分
|
||
|
||
<G3ApiParams title="错误码" variant="bare">
|
||
|
||
| code | 含义 | 典型原因 | `data` 里的定位信息 |
|
||
|---|---|---|---|
|
||
| `2001` | 保存失败 | 字段不存在、类型不匹配、数据库约束冲突等 | `table` / `action` / `index` / `cause` |
|
||
| `2002` | 前置条件不成立 | `guards` 里的条件没满足 | `table` / `action` / `index` |
|
||
| `2003` | 并发编辑冲突 | `$original` 的原值已被别人改过、`b_row_version` 不匹配 | `table` / `action` / `index` / `fields` |
|
||
|
||
</G3ApiParams>
|
||
|
||
`2003` 的正确处理不是重试,而是:**重新拉取该行**,用 `data.fields` 标出哪些字段已被他人修改,让用户决定覆盖还是放弃。
|
||
|
||
`action` 的取值是 `update` / `insert` / `delete`,`index` 是请求数组里的下标(从 `0` 开始)—— 批量保存失败时靠它定位到具体哪一行。
|
||
|
||
## `POST` `/data/delete`
|
||
|
||
独立删除接口。和 `saveobjt` 的区别是它**不进保存事务**、也不涉及增改。
|
||
|
||
<G3ApiParams>
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `target` | string | 是 | `module` 或 `direct`,其它值报 `1000` |
|
||
| `moduleCode` | string | 是(`target=module`) | 模块编码(`s_module.b_id`) |
|
||
| `table` | string | 是(`target=direct`) | 表名 |
|
||
| `key_field` | string | 是(`target=direct`) | 主键列名 |
|
||
| `ids` | any[] | | 要删除的主键值;`direct` 下为空数组会直接返回 `{}` |
|
||
|
||
</G3ApiParams>
|
||
|
||
按模块删除(会按模块配置联动删除子表):
|
||
|
||
<G3ApiPayload title="请求体">
|
||
|
||
```json
|
||
{
|
||
"target": "module",
|
||
"moduleCode": "othercompany",
|
||
"ids": ["C001", "C002"]
|
||
}
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
指定表直接删除:
|
||
|
||
<G3ApiPayload title="请求体">
|
||
|
||
```json
|
||
{
|
||
"target": "direct",
|
||
"table": "b_othercompany",
|
||
"key_field": "b_id",
|
||
"ids": ["C001"]
|
||
}
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
<G3ApiPayload title="响应" :code="0">
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "操作成功",
|
||
"data": { "b_othercompany": 2 }
|
||
}
|
||
```
|
||
|
||
</G3ApiPayload>
|
||
|
||
`data` 是 **`{表名: 实际删除行数}`** —— `direct` 只有一项;`module` 会带出所有被联动删除的表,所以可能有多项。整个删除同样在单个事务里完成。
|
||
|
||
> `target` 必须显式声明。后端**刻意不做**「没传 `moduleCode` 就当直删」这种隐式分支,避免漏传参数时静默绕过模块规则。
|