# 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 type SqlParams = Record interface QueryRequest { sql: string params?: SqlParams } interface QueryResult> { rows: T[] rowCount: number } interface TransactionRequest { operations: SqlOperation[] } interface SqlOperation { sql: string params?: SqlParams expectAffectedRows?: number } interface TransactionResult { steps: TransactionStepResult[] } interface TransactionStepResult> { index: number affectedRows: number | null rows: T[] } interface ApiResponse { code: number message: string data?: T details?: Record } ```