From 1b54c92f7a61ed2f4d9e8d28ee57989baa228e6c Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Tue, 14 Jul 2026 17:21:56 +0800 Subject: [PATCH] 20260714172155 --- code/fms/REQUIREMENTS.md | 505 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 code/fms/REQUIREMENTS.md diff --git a/code/fms/REQUIREMENTS.md b/code/fms/REQUIREMENTS.md new file mode 100644 index 00000000..39ec34ff --- /dev/null +++ b/code/fms/REQUIREMENTS.md @@ -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 +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 +} +```