Files
workspace/code/fms/REQUIREMENTS.md
T
2026-07-14 17:21:56 +08:00

12 KiB

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 中的表名、字段名、函数、表达式和排序规则由前端直接编写。

示例:

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

对应参数:

{
  "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 字段

数组参数示例:

{
  "sql": "SELECT * FROM v_booking_list WHERE id IN (:ids)",
  "params": {
    "ids": [101, 102, 103]
  }
}

数组参数规则:

  • 数组参数在执行时展开为多个绑定参数。
  • 数组元素必须是标量值或 null,不能是对象或嵌套数组。
  • 空数组不能用于参数展开;前端应直接调整 SQL 条件。

当数据库无法自动确定参数类型时,前端可以在 SQL 中显式转换:

WHERE booking_id = CAST(:bookingId AS bigint)

2.3 通用成功响应

{
  "code": 0,
  "message": "ok",
  "data": {}
}

2.4 通用失败响应

{
  "code": 1002,
  "message": "missing sql parameter",
  "details": {
    "parameter": "status"
  }
}

约定:

  • code = 0 表示成功。
  • HTTP 状态码表示错误类别,code 表示具体错误。
  • details 为可选信息。
  • 响应不能包含完整 SQL、数据库连接信息或服务端调用栈。

3. 查询接口

3.1 请求

POST /api/query
Content-Type: application/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 查询响应

{
  "code": 0,
  "message": "ok",
  "data": {
    "rows": [
      {
        "id": 128,
        "booking_no": "BK001",
        "customer_name": "长荣海运",
        "total_amount": 3500
      }
    ],
    "rowCount": 1
  }
}

规则:

  • rows 始终是数组。
  • 没有记录时返回空数组。
  • rowCount 是本次实际返回的记录数,不代表忽略分页后的总数。
  • 分页、总数和排序均由前端 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 请求

POST /api/transaction
Content-Type: application/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 主键:

{
  "sql": "INSERT INTO booking (id, booking_no) VALUES (:id, :bookingNo) RETURNING id",
  "params": {
    "id": "05f0c596-73d7-4f78-a115-d4591bff6e62",
    "bookingNo": "BK001"
  }
}

数据库生成主键:

{
  "sql": "INSERT INTO booking (booking_no) VALUES (:bookingNo) RETURNING id",
  "params": {
    "bookingNo": "BK001"
  }
}

4.4 步骤引用

后续步骤的参数可以引用前序步骤返回的数据:

{
  "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
    }
  ]
}

引用格式:

{{steps.<步骤索引>.rows.<记录索引>.<字段名>}}

规则:

  • 步骤索引和记录索引从 0 开始。
  • 只能引用当前步骤之前的结果。
  • 被引用字段必须由前序 SQL 查询或 RETURNING 返回。
  • 占位符必须构成完整参数值,不能嵌入普通字符串。
  • 替换后保留原始数据类型。
  • 引用不存在的步骤、记录或字段时,事务回滚。
  • 步骤引用只能出现在 params 中,不能用于动态拼接 SQL。

4.5 并发控制

接口不规定业务并发策略。前端可以通过 SQL 条件和 expectAffectedRows 实现乐观锁:

{
  "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 事务响应

{
  "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 或事务执行超时

事务失败响应:

{
  "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 类型参考

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>
}