20260714172155

This commit is contained in:
oneao committed 2026-07-14 17:21:56 +08:00
1 parent 079068eac9
commit 1b54c92f7a
1 file changed
+505
+505
View File
@@ -0,0 +1,505 @@
# FMS 通用 SQL API 规范
## 1. 设计定位
FMS 采用前端主导、数据库驱动的设计:
- 前端负责业务流程、页面权限、字段权限和业务校验。
- 数据库表保存业务数据,数据库视图负责关联、聚合和查询模型。
- 后端不定义业务实体,不读取表结构,不维护业务接口。
- 后端只负责参数绑定、SQL 执行、事务控制和统一响应。
- 数据库约束负责最终的数据完整性。
系统只提供两个数据端点:
| 方法 | 路径 | 用途 |
|------|------|------|
| `POST` | `/api/query` | 执行单条只读 SQL |
| `POST` | `/api/transaction` | 在同一事务中执行多条 SQL |
身份认证、业务权限和页面控制不属于本文档范围。
## 2. 通用约定
### 2.1 请求格式
- 请求和响应使用 `application/json; charset=utf-8`。
- SQL 由前端提供。
- SQL 中的数据值必须使用命名参数,不能直接拼接用户输入。
- 命名参数格式为 `:参数名`。
- 参数名格式为 `^[A-Za-z_][A-Za-z0-9_]*$`。
- 同一个命名参数可以在一条 SQL 中重复使用。
- SQL 中的表名、字段名、函数、表达式和排序规则由前端直接编写。
示例:
```sql
SELECT id, booking_no, status
FROM v_booking_list
WHERE status = :status
AND created_at >= :startTime
ORDER BY created_at DESC
LIMIT :limit OFFSET :offset
```
对应参数:
```json
{
"status": "active",
"startTime": "2026-01-01T00:00:00+08:00",
"limit": 20,
"offset": 0
}
```
### 2.2 参数类型
参数支持以下 JSON 类型:
| JSON 类型 | 说明 |
|-----------|------|
| string | 文本、日期时间、UUID、精确小数文本 |
| number | 整数和普通小数 |
| boolean | 布尔值 |
| null | SQL NULL |
| array | 用于 `IN` 等列表参数 |
| object | 用于写入 PostgreSQL JSON/JSONB 字段 |
数组参数示例:
```json
{
"sql": "SELECT * FROM v_booking_list WHERE id IN (:ids)",
"params": {
"ids": [101, 102, 103]
}
}
```
数组参数规则:
- 数组参数在执行时展开为多个绑定参数。
- 数组元素必须是标量值或 `null`,不能是对象或嵌套数组。
- 空数组不能用于参数展开;前端应直接调整 SQL 条件。
当数据库无法自动确定参数类型时,前端可以在 SQL 中显式转换:
```sql
WHERE booking_id = CAST(:bookingId AS bigint)
```
### 2.3 通用成功响应
```json
{
"code": 0,
"message": "ok",
"data": {}
}
```
### 2.4 通用失败响应
```json
{
"code": 1002,
"message": "missing sql parameter",
"details": {
"parameter": "status"
}
}
```
约定:
- `code = 0` 表示成功。
- HTTP 状态码表示错误类别,`code` 表示具体错误。
- `details` 为可选信息。
- 响应不能包含完整 SQL、数据库连接信息或服务端调用栈。
## 3. 查询接口
### 3.1 请求
```http
POST /api/query
Content-Type: application/json
```
请求结构:
```json
{
"sql": "SELECT id, booking_no, customer_name, total_amount FROM v_booking_list WHERE status = :status ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
"params": {
"status": "active",
"limit": 20,
"offset": 0
}
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `sql` | string | 是 | 单条只读 SQL |
| `params` | object | 否 | 命名参数;SQL 无参数时可省略 |
### 3.2 SQL 范围
查询接口允许:
- `SELECT`
- 所有 CTE 都是只读查询的 `WITH`
- 查询表或数据库视图
- 关联、子查询、窗口函数、聚合和数据库函数
查询接口不允许:
- `INSERT`、`UPDATE`、`DELETE`、`MERGE`
- `CREATE`、`ALTER`、`DROP`、`TRUNCATE`
- `GRANT`、`REVOKE`
- `CALL`、`DO` 或其他可能修改数据的语句
- 显式事务控制语句
- 一次请求执行多条 SQL
SQL 末尾可以有一个分号,但不能通过分号附加第二条语句。
即使最外层是 `SELECT`,查询接口也不能在 CTE 中执行写入。
### 3.3 查询响应
```json
{
"code": 0,
"message": "ok",
"data": {
"rows": [
{
"id": 128,
"booking_no": "BK001",
"customer_name": "长荣海运",
"total_amount": 3500
}
],
"rowCount": 1
}
}
```
规则:
- `rows` 始终是数组。
- 没有记录时返回空数组。
- `rowCount` 是本次实际返回的记录数,不代表忽略分页后的总数。
- 分页、总数和排序均由前端 SQL 决定。
- 需要总数时可以使用窗口函数或单独执行查询。
分页示例:
```sql
SELECT
v.*,
COUNT(*) OVER () AS total_count
FROM v_booking_list v
WHERE v.status = :status
ORDER BY v.created_at DESC
LIMIT :limit OFFSET :offset
```
## 4. 事务接口
### 4.1 请求
```http
POST /api/transaction
Content-Type: application/json
```
请求结构:
```json
{
"operations": [
{
"sql": "INSERT INTO booking (id, booking_no, customer_id, status) VALUES (:id, :bookingNo, :customerId, :status) RETURNING id",
"params": {
"id": "05f0c596-73d7-4f78-a115-d4591bff6e62",
"bookingNo": "BK001",
"customerId": 42,
"status": "pending"
},
"expectAffectedRows": 1
}
]
}
```
事务规则:
- `operations` 必须是非空数组。
- 操作按照数组顺序执行。
- 所有操作使用同一个数据库事务。
- 任一步骤失败,整个事务回滚。
- 每个 operation 只能包含一条 SQL。
- 不允许显式提交、回滚或创建嵌套事务。
### 4.2 Operation 结构
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `sql` | string | 是 | 单条 SQL |
| `params` | object | 否 | 命名参数 |
| `expectAffectedRows` | integer | 否 | 实际影响行数必须等于该值 |
事务接口允许:
- `SELECT`
- `INSERT`
- `UPDATE`
- `DELETE`
- 上述语句使用的 `WITH`
- `RETURNING`
事务接口不允许:
- DDL 语句
- 权限管理语句
- 显式事务控制语句
- `CALL`、`DO` 等不透明过程调用
- 一次 operation 执行多条 SQL
### 4.3 主键
后端不识别或推断主键。主键字段和值由前端直接写入 SQL 和参数。
前端生成 UUID 主键:
```json
{
"sql": "INSERT INTO booking (id, booking_no) VALUES (:id, :bookingNo) RETURNING id",
"params": {
"id": "05f0c596-73d7-4f78-a115-d4591bff6e62",
"bookingNo": "BK001"
}
}
```
数据库生成主键:
```json
{
"sql": "INSERT INTO booking (booking_no) VALUES (:bookingNo) RETURNING id",
"params": {
"bookingNo": "BK001"
}
}
```
### 4.4 步骤引用
后续步骤的参数可以引用前序步骤返回的数据:
```json
{
"operations": [
{
"sql": "INSERT INTO booking (booking_no, customer_id) VALUES (:bookingNo, :customerId) RETURNING id",
"params": {
"bookingNo": "BK001",
"customerId": 42
},
"expectAffectedRows": 1
},
{
"sql": "INSERT INTO booking_container (booking_id, container_no) VALUES (:bookingId, :containerNo) RETURNING id",
"params": {
"bookingId": "{{steps.0.rows.0.id}}",
"containerNo": "MSKU001"
},
"expectAffectedRows": 1
},
{
"sql": "INSERT INTO fee (booking_id, fee_type, amount, currency) VALUES (:bookingId, :feeType, :amount, :currency) RETURNING id",
"params": {
"bookingId": "{{steps.0.rows.0.id}}",
"feeType": "ocean_freight",
"amount": 3500,
"currency": "USD"
},
"expectAffectedRows": 1
}
]
}
```
引用格式:
```text
{{steps.<步骤索引>.rows.<记录索引>.<字段名>}}
```
规则:
- 步骤索引和记录索引从 0 开始。
- 只能引用当前步骤之前的结果。
- 被引用字段必须由前序 SQL 查询或 `RETURNING` 返回。
- 占位符必须构成完整参数值,不能嵌入普通字符串。
- 替换后保留原始数据类型。
- 引用不存在的步骤、记录或字段时,事务回滚。
- 步骤引用只能出现在 `params` 中,不能用于动态拼接 SQL。
### 4.5 并发控制
接口不规定业务并发策略。前端可以通过 SQL 条件和 `expectAffectedRows` 实现乐观锁:
```json
{
"sql": "UPDATE booking SET status = :newStatus, version = version + 1 WHERE id = :id AND version = :version RETURNING id, status, version",
"params": {
"id": 128,
"version": 3,
"newStatus": "confirmed"
},
"expectAffectedRows": 1
}
```
当实际影响行数不等于 `expectAffectedRows` 时,该步骤失败,整个事务回滚。
### 4.6 事务响应
```json
{
"code": 0,
"message": "ok",
"data": {
"steps": [
{
"index": 0,
"affectedRows": 1,
"rows": [
{ "id": 128 }
]
},
{
"index": 1,
"affectedRows": 1,
"rows": [
{ "id": 301 }
]
}
]
}
}
```
响应规则:
- `steps` 顺序与请求中的 `operations` 一致。
- `rows` 始终是数组。
- `SELECT` 返回查询结果。
- 带 `RETURNING` 的写入语句返回 `RETURNING` 结果。
- 不带 `RETURNING` 的写入语句返回空数组。
- `affectedRows` 表示写入影响行数;纯查询步骤返回 `null`。
## 5. 错误规范
| HTTP 状态码 | code | 说明 |
|-------------|------|------|
| 400 | 1001 | 请求 JSON 或字段结构错误 |
| 400 | 1002 | SQL 参数缺失、重复冲突或存在未使用参数 |
| 400 | 1003 | SQL 为空或包含多条语句 |
| 400 | 1004 | 当前端点不允许该 SQL 类型 |
| 400 | 1005 | 参数类型或数组参数格式错误 |
| 400 | 1006 | SQL 长度、步骤数、参数数或请求大小超限 |
| 409 | 2001 | `expectAffectedRows` 与实际影响行数不一致 |
| 409 | 2002 | 唯一键、外键、非空或检查约束冲突 |
| 409 | 2003 | 死锁、序列化失败或其他并发冲突 |
| 422 | 3001 | SQL 语法错误 |
| 422 | 3002 | 表、视图、字段或函数不存在 |
| 422 | 3003 | 参数值无法转换为数据库字段类型 |
| 422 | 3004 | 步骤引用无法解析 |
| 500 | 9001 | 数据库执行失败 |
| 500 | 9002 | 事务提交失败 |
| 504 | 9003 | SQL 或事务执行超时 |
事务失败响应:
```json
{
"code": 2002,
"message": "database constraint violation",
"details": {
"operationIndex": 1,
"constraint": "booking_container_booking_id_fkey"
}
}
```
规则:
- 事务执行错误必须返回 `operationIndex`。
- 可以返回数据库约束名称,但不能返回完整 SQL。
- 无法分类的数据库错误统一返回 `9001`。
- 事务失败时所有步骤均视为未提交。
## 6. 执行限制
默认限制:
| 项目 | 限制 |
|------|------|
| 单条 SQL 长度 | 100 KB |
| 单次请求大小 | 2 MB |
| query 最大返回行数 | 5000 行 |
| transaction 最大步骤数 | 100 个 |
| 单条 SQL 最大参数数 | 5000 个 |
| query 执行超时 | 30 秒 |
| transaction 执行超时 | 60 秒 |
超过返回行数限制时请求失败,不静默截断结果。大数据导出应使用专门的导出机制,不通过本接口一次性返回。
## 7. TypeScript 类型参考
```typescript
type SqlScalar = string | number | boolean | null
type SqlParam = SqlScalar | SqlScalar[] | Record<string, unknown>
type SqlParams = Record<string, SqlParam>
interface QueryRequest {
sql: string
params?: SqlParams
}
interface QueryResult<T = Record<string, unknown>> {
rows: T[]
rowCount: number
}
interface TransactionRequest {
operations: SqlOperation[]
}
interface SqlOperation {
sql: string
params?: SqlParams
expectAffectedRows?: number
}
interface TransactionResult {
steps: TransactionStepResult[]
}
interface TransactionStepResult<T = Record<string, unknown>> {
index: number
affectedRows: number | null
rows: T[]
}
interface ApiResponse<T> {
code: number
message: string
data?: T
details?: Record<string, unknown>
}
```