Files
workspace/code/g3soft-libs/docs/content/api/save.md
T
2026-10-12 22:25:41 +08:00

214 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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` 就当直删」这种隐式分支,避免漏传参数时静默绕过模块规则。