diff --git a/code/fms/REQUIREMENTS.md b/code/fms/REQUIREMENTS.md deleted file mode 100644 index 1109ccaf..00000000 --- a/code/fms/REQUIREMENTS.md +++ /dev/null @@ -1,870 +0,0 @@ -# FMS 后端 API 需求规范 - -## 1. 设计定位 - -FMS 是面向内部用户的多机构 ERP 系统。 - -当前阶段只确定以下基础原则: - -- 所有 API 请求统一使用 `POST`。 -- 前端登录时通过请求头传递机构码。 -- 一个机构对应一个独立数据库。 -- 登录成功后机构码绑定到访问令牌,后续请求由后端从令牌获取机构码并选择数据库。 -- 前端不接触数据库连接信息。 -- 后端负责身份认证、机构数据库路由和统一响应。 -- 权限编码只用于前端禁用按钮,暂不作为后端业务接口的授权条件。 -- 普通查询和保存采用结构化数据协议,不要求前端编写参数占位符。 -- 高级只读查询可以通过独立的 `load_sql` 接口提交 SQL。 -- 新业务在现有数据库表和视图能够支持的情况下,只需开发前端页面,不需要增加后端业务接口。 - -当前提供以下接口: - -| 方法 | 路径 | 用途 | 是否需要登录 | -|------|------|------|--------------| -| `POST` | `/api/auth/login` | 登录并获取用户及按钮权限 | 否 | -| `POST` | `/api/data/load` | 通用结构化数据查询 | 是 | -| `POST` | `/api/data/load_sql` | 高级只读 SQL 查询 | 是 | -| `POST` | `/api/data/save` | 通用数据保存 | 是 | - -## 2. 通用请求规范 - -### 2.1 HTTP 方法 - -- 所有接口只使用 `POST`。 -- 不使用 `GET`、`PUT`、`PATCH` 或 `DELETE`。 -- 查询、创建、修改、删除和业务操作都通过后续定义的 `POST` 接口完成。 - -### 2.2 数据格式 - -- 请求和响应使用 `application/json; charset=utf-8`。 -- 除登录路由使用的 `orgid` 请求头外,请求参数放在 JSON 请求体中。 -- 需要登录的接口通过 Bearer Token 认证。 -- 响应不能包含数据库连接信息、令牌密钥、密码或服务端调用栈。 - -需要登录的请求必须包含: - -```http -Authorization: Bearer -Content-Type: application/json; charset=utf-8 -``` - -### 2.3 命名规范 - -- API 路径、请求字段和响应字段优先使用简短、明确的全小写名称。 -- 能使用单个单词或常用组合词时,不使用驼峰命名法,例如 `username`、`orgid`。 -- 多个单词无法简洁表达时使用 `snake_case`,例如 `access_token`、`display_name`。 -- JSON 字段不使用 `camelCase`,例如不使用 `accessToken`、`displayName`。 -- TypeScript 接口和类型名称遵循语言惯例使用 `PascalCase`,其字段名与 API JSON 字段保持一致。 - -### 2.4 机构标识 - -机构标识统一命名为 `orgid`。 - -规则: - -- `orgid` 类型为非空字符串。 -- 登录请求必须通过 `orgid` 请求头传递机构标识。 -- 登录成功后,`orgid` 保存在访问令牌声明或服务端会话中。 -- 后续业务请求不在请求头或请求体中重复传递 `orgid`。 -- 后端从访问令牌中获取 `orgid` 并选择机构数据库。 -- 前端只传机构码,不能传数据库名称、地址、端口、账号或密码。 -- 机构码必须作为完整字段值传递,不能用于拼接数据库连接字符串或 SQL。 -- 机构码不存在、已停用或未配置数据库时,请求失败。 - -登录请求头示例: - -```http -orgid: SH001 -``` - -### 2.5 通用成功响应 - -```json -{ - "code": 0, - "message": "ok", - "data": {} -} -``` - -### 2.6 通用失败响应 - -```json -{ - "code": 1201, - "message": "organization not found", - "details": { - "orgid": "SH001" - } -} -``` - -约定: - -- `code = 0` 表示成功。 -- HTTP 状态码表示错误类别,`code` 表示具体错误。 -- `data` 只在成功且接口有返回数据时出现。 -- `details` 为可选的结构化错误信息。 - -## 3. 机构数据库路由 - -### 3.1 数据隔离模型 - -FMS 采用一个机构一个数据库的隔离方式: - -```text -orgid: SH001 -> database: fms_sh001 -orgid: BJ001 -> database: fms_bj001 -orgid: GZ001 -> database: fms_gz001 -``` - -上述映射只表示逻辑关系,真实数据库名称和连接信息不能返回给前端。 - -### 3.2 后端处理流程 - -后端收到请求后按以下顺序处理: - -1. 登录时从 `orgid` 请求头获取机构标识;后续请求从访问令牌获取机构标识。 -2. 校验 `orgid` 的格式。 -3. 从服务端机构配置中查找机构。 -4. 检查机构是否启用以及数据库配置是否可用。 -5. 获取该机构对应的数据库连接或连接池。 -6. 在该机构数据库中完成认证或后续业务操作。 -7. 返回统一响应,不暴露数据库连接信息。 - -### 3.3 登录状态与机构绑定 - -- 登录成功后,访问令牌必须绑定当前 `orgid` 和用户标识。 -- 后续请求只以访问令牌绑定的 `orgid` 为准。 -- 后续请求不能通过请求头或请求体覆盖令牌中的 `orgid`。 -- 用户需要切换机构时,必须在登录请求头中传入目标 `orgid` 并重新登录。 -- 当前不提供 refresh;访问令牌过期后需要重新登录。 -- 当前不提供 logout;客户端退出时删除本地访问令牌。 - -### 3.4 连接管理 - -- 数据库连接配置由后端集中管理。 -- 后端可以为每个机构维护独立连接池。 -- 同一次请求只能访问一个机构数据库。 -- 机构数据库不可用时,不能自动切换到其他机构数据库。 -- 日志可以记录机构码,但不能记录数据库密码或完整连接字符串。 - -## 4. 登录接口 - -### 4.1 登录页面 - -登录页面必须提供以下输入项: - -| 输入项 | 必填 | 说明 | -|--------|------|------| -| 机构码 | 是 | 用户所属机构的 `orgid` | -| 用户名 | 是 | 用户登录账号 | -| 密码 | 是 | 用户登录密码 | - -用户点击登录后,前端将机构码放入 `orgid` 请求头,将用户名和密码放入 JSON 请求体。 - -### 4.2 请求 - -```http -POST /api/auth/login -orgid: SH001 -Content-Type: application/json; charset=utf-8 -``` - -请求体: - -```json -{ - "username": "zhangsan", - "password": "password" -} -``` - -| 字段 | 位置 | 类型 | 必填 | 说明 | -|------|------|------|------|------| -| `orgid` | header | string | 是 | 登录页面填写的机构码,用于选择机构数据库 | -| `username` | body | string | 是 | 登录账号 | -| `password` | body | string | 是 | 登录密码 | - -### 4.3 处理规则 - -- 后端先根据 `orgid` 选择机构数据库,再验证用户名和密码。 -- 登录失败时不能说明账号是否存在。 -- 密码不能写入日志或错误响应。 -- 登录成功后令牌绑定机构码和用户标识。 -- 当前不签发 refresh token。 - -### 4.4 成功响应 - -```json -{ - "code": 0, - "message": "ok", - "data": { - "access_token": "access-token", - "token_type": "Bearer", - "expires_in": 1800, - "organization": { - "orgid": "SH001", - "name": "上海分公司" - }, - "user": { - "id": 1001, - "username": "zhangsan", - "display_name": "张三" - }, - "permissions": [ - "sales.order.create", - "sales.order.approve", - "finance.payment.view" - ] - } -} -``` - -## 5. 前端按钮权限 - -- `permissions` 是当前登录用户拥有的权限编码数组。 -- 权限编码由机构数据库中的用户、角色和权限配置产生。 -- 权限编码建议使用 `<模块>.<资源>.<动作>` 格式。 -- 前端根据权限编码设置按钮的禁用状态。 -- 用户没有对应权限时,按钮保持可见但不可操作。 -- 当前权限只控制前端交互,不作为后端业务授权条件。 - -前端判断示例: - -```typescript -const canApprove = permissions.includes('sales.order.approve') -``` - -## 6. 通用加载接口 - -### 6.1 设计原则 - -- 前端提交数据源、返回字段、过滤条件、排序和分页。 -- 前端不提交 SQL,也不编写参数占位符。 -- 后端根据结构化请求生成查询 SQL,并在后端内部绑定所有数据值。 -- 查询数据源可以是数据库表或视图。 -- 复杂关联、聚合和计算字段优先封装为数据库视图,再通过本接口查询。 -- 后端不为具体业务定义查询接口。 - -### 6.2 请求 - -```http -POST /api/data/load -Authorization: Bearer -Content-Type: application/json; charset=utf-8 -``` - -请求示例: - -```json -{ - "source": "v_sales_order_list", - "fields": [ - "id", - "order_no", - "customer_name", - "status", - "total_amount", - "created_at" - ], - "filter": { - "logic": "and", - "items": [ - { - "field": "status", - "op": "eq", - "value": "pending" - }, - { - "logic": "or", - "items": [ - { - "field": "customer_name", - "op": "contains", - "value": "海运" - }, - { - "field": "order_no", - "op": "starts_with", - "value": "SO2026" - } - ] - } - ] - }, - "sort": [ - { - "field": "created_at", - "direction": "desc" - } - ], - "page": { - "page_no": 1, - "page_size": 20 - }, - "include_total": true -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `source` | string | 是 | 数据库表名或视图名 | -| `fields` | string[] | 是 | 返回字段,必须是非空数组 | -| `filter` | object | 否 | 过滤条件组 | -| `sort` | array | 否 | 排序规则 | -| `page` | object | 否 | 分页;省略时使用默认分页 | -| `include_total` | boolean | 否 | 是否计算符合条件的总记录数,默认 `false` | - -### 6.3 标识符规则 - -- `source`、字段名和排序字段只能是数据库标识符,不能包含 SQL 表达式。 -- 标识符必须符合 `^[a-z_][a-z0-9_]*$`。 -- `source` 可以包含一个 schema 前缀,例如 `public.v_sales_order_list`。 -- 带 schema 的数据源必须符合 `.` 格式。 -- 后端必须正确引用标识符,不能把标识符当作数据值拼接。 -- 字段必须真实存在于指定数据源中。 -- `fields` 只返回前端明确请求的字段,不支持 `*`。 - -### 6.4 过滤条件 - -过滤条件分为条件项和条件组。 - -条件项: - -```json -{ - "field": "status", - "op": "eq", - "value": "pending" -} -``` - -条件组: - -```json -{ - "logic": "and", - "items": [ - { - "field": "status", - "op": "eq", - "value": "pending" - }, - { - "field": "total_amount", - "op": "gte", - "value": 1000 - } - ] -} -``` - -条件组规则: - -- `logic` 只允许 `and` 或 `or`。 -- `items` 必须是非空数组。 -- 条件组可以嵌套条件组。 -- 所有 `value` 都由后端内部参数绑定,不作为 SQL 文本执行。 - -支持的操作符: - -| `op` | 说明 | `value` 规则 | -|------|------|--------------| -| `eq` | 等于 | 单个值 | -| `ne` | 不等于 | 单个值 | -| `gt` | 大于 | 单个值 | -| `gte` | 大于或等于 | 单个值 | -| `lt` | 小于 | 单个值 | -| `lte` | 小于或等于 | 单个值 | -| `in` | 在列表中 | 非空数组 | -| `not_in` | 不在列表中 | 非空数组 | -| `between` | 在范围内,包含边界 | 两个元素的数组 | -| `contains` | 文本包含 | string | -| `starts_with` | 文本开头匹配 | string | -| `ends_with` | 文本结尾匹配 | string | -| `is_null` | 为 `NULL` | 不传 `value` | -| `is_not_null` | 不为 `NULL` | 不传 `value` | - -补充规则: - -- `eq` 的值为 `null` 时按 `is_null` 处理。 -- `ne` 的值为 `null` 时按 `is_not_null` 处理。 -- `contains`、`starts_with` 和 `ends_with` 中的 `%`、`_` 默认作为普通字符处理。 -- 空数组不能用于 `in`、`not_in` 或 `between`。 - -### 6.5 排序和分页 - -- `direction` 只允许 `asc` 或 `desc`。 -- 可以指定多个排序字段,后端按数组顺序生成排序规则。 -- `page_no` 从 1 开始,默认值为 1。 -- `page_size` 默认值为 20,最大值为 5000。 -- 前端需要稳定分页时,应包含主键或其他唯一字段作为最后一个排序字段。 - -### 6.6 成功响应 - -```json -{ - "code": 0, - "message": "ok", - "data": { - "rows": [ - { - "id": 128, - "order_no": "SO20260001", - "customer_name": "长荣海运", - "status": "pending", - "total_amount": 3500, - "created_at": "2026-07-14T10:30:00+08:00" - } - ], - "row_count": 1, - "total": 36, - "page_no": 1, - "page_size": 20 - } -} -``` - -响应规则: - -- `rows` 始终是数组,没有记录时返回空数组。 -- `row_count` 是本次实际返回的记录数。 -- `total` 只在 `include_total = true` 时返回,否则为 `null`。 -- `total` 表示忽略分页后符合过滤条件的记录数。 - -## 7. 高级 SQL 加载接口 - -### 7.1 适用范围 - -`/api/data/load_sql` 用于结构化 `/api/data/load` 无法表达的高级只读查询,例如复杂关联、CTE、窗口函数和聚合查询。 - -规则: - -- 该接口是可选的高级能力。 -- 只允许执行单条只读 SQL。 -- 允许 `SELECT`,以及所有 CTE 都是只读查询的 `WITH`。 -- 不允许 `INSERT`、`UPDATE`、`DELETE`、`MERGE`、DDL、权限管理和事务控制语句。 -- 不允许通过分号附加第二条语句。 -- 即使最外层是 `SELECT`,也不允许在 CTE 中写入数据。 -- SQL 不支持参数占位符,前端不能把最终用户输入直接拼接到 SQL 中。 -- 包含页面动态筛选值的查询应优先使用 `/api/data/load`。 -- 返回行数和执行超时遵循普通加载接口限制。 - -### 7.2 请求 - -```http -POST /api/data/load_sql -Authorization: Bearer -Content-Type: application/json; charset=utf-8 -``` - -请求示例: - -```json -{ - "sql": "SELECT customer_id, customer_name, COUNT(*) AS order_count, SUM(total_amount) AS total_amount FROM v_sales_order_list GROUP BY customer_id, customer_name ORDER BY total_amount DESC LIMIT 100" -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `sql` | string | 是 | 单条只读 SQL | - -### 7.3 成功响应 - -```json -{ - "code": 0, - "message": "ok", - "data": { - "rows": [ - { - "customer_id": 42, - "customer_name": "长荣海运", - "order_count": 18, - "total_amount": 63000 - } - ], - "row_count": 1 - } -} -``` - -响应规则: - -- `rows` 始终是数组,没有记录时返回空数组。 -- `row_count` 是本次实际返回的记录数。 -- 排序、分页和总数由 SQL 自身决定。 - -## 8. 通用保存接口 - -### 8.1 设计原则 - -- 保存接口支持在一次请求中新增、修改或删除多条数据。 -- 前端提交目标表、操作类型、数据值和过滤条件。 -- 前端不提交 SQL,也不编写参数占位符。 -- 后端根据结构化请求生成 SQL,并在后端内部绑定所有数据值。 -- 一个请求中的所有变更使用同一个数据库事务。 -- 任一变更失败时,整个请求回滚。 -- 后端不为具体业务定义保存接口。 - -### 8.2 请求 - -```http -POST /api/data/save -Authorization: Bearer -Content-Type: application/json; charset=utf-8 -``` - -请求示例: - -```json -{ - "changes": [ - { - "action": "insert", - "target": "sales_order", - "values": { - "id": "019f62d5-4115-7b22-8c51-8f89e2e8b421", - "order_no": "SO20260001", - "customer_id": 42, - "status": "draft", - "version": 1 - }, - "returning": [ - "id", - "order_no" - ], - "expect_rows": 1 - }, - { - "action": "insert", - "target": "sales_order_item", - "values": { - "id": "019f62d5-4115-79db-ab88-99e8c5dc7f21", - "order_id": "019f62d5-4115-7b22-8c51-8f89e2e8b421", - "product_id": 301, - "quantity": 2, - "unit_price": 1750 - }, - "returning": [ - "id" - ], - "expect_rows": 1 - } - ] -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `changes` | array | 是 | 按顺序执行的变更,必须是非空数组 | - -### 8.3 Change 结构 - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `action` | string | 是 | `insert`、`update` 或 `delete` | -| `target` | string | 是 | 目标表或可写视图 | -| `values` | object | insert、update 必填 | 要写入的字段和值 | -| `filter` | object | update、delete 必填 | 确定修改或删除范围的过滤条件 | -| `returning` | string[] | 否 | 保存后需要返回的字段 | -| `expect_rows` | integer | 否 | 实际影响行数必须等于该非负整数 | - -### 8.4 保存规则 - -- `target` 和字段名遵循查询接口的标识符规则。 -- `values` 必须是非空对象。 -- `values` 的值可以是 string、number、boolean、null、array 或 object。 -- array 和 object 作为数据库字段值时,由数据库字段类型决定如何转换。 -- `update` 和 `delete` 必须提供非空 `filter`,不允许无条件修改或删除整表数据。 -- 保存过滤条件使用查询接口定义的条件结构和操作符。 -- `returning` 省略时返回空数组。 -- `expect_rows` 不匹配时整个请求失败并回滚。 -- 数据库默认值通过省略对应字段触发。 -- `values` 不支持 SQL 函数、字段引用或其他原始 SQL 表达式。 - -### 8.5 多表保存和主键 - -保存接口不提供步骤结果占位符,也不支持后续变更引用前序变更的返回值。 - -多表新增时,前端应提前生成 UUID 主键,并把主键和外键直接写入各个 change 的 `values`: - -```text -前端生成订单 id --> 前端把订单 id 写入订单数据 --> 前端把同一个订单 id 写入明细的 order_id --> 一次提交所有 changes --> 后端在同一事务中保存 -``` - -如果某个表必须使用数据库生成的主键,则只能在本次请求的 `returning` 中获取,不能在同一次保存请求的后续 change 中引用。需要多表原子保存时,应优先使用前端生成的 UUID 主键。 - -### 8.6 修改和乐观锁示例 - -```json -{ - "changes": [ - { - "action": "update", - "target": "sales_order", - "values": { - "status": "approved", - "version": 4 - }, - "filter": { - "logic": "and", - "items": [ - { - "field": "id", - "op": "eq", - "value": "019f62d5-4115-7b22-8c51-8f89e2e8b421" - }, - { - "field": "version", - "op": "eq", - "value": 3 - } - ] - }, - "returning": [ - "id", - "status", - "version" - ], - "expect_rows": 1 - } - ] -} -``` - -前端读取当前版本号,保存时同时提交旧版本条件和新版本值。当其他用户已经修改记录时,`expect_rows` 检查失败,整个请求回滚。 - -### 8.7 成功响应 - -```json -{ - "code": 0, - "message": "ok", - "data": { - "results": [ - { - "index": 0, - "action": "insert", - "affected_rows": 1, - "rows": [ - { - "id": "019f62d5-4115-7b22-8c51-8f89e2e8b421", - "order_no": "SO20260001" - } - ] - }, - { - "index": 1, - "action": "insert", - "affected_rows": 1, - "rows": [ - { - "id": "019f62d5-4115-79db-ab88-99e8c5dc7f21" - } - ] - } - ] - } -} -``` - -响应规则: - -- `results` 顺序与请求中的 `changes` 一致。 -- `rows` 始终是数组。 -- `affected_rows` 表示该变更实际影响的记录数。 -- 事务提交成功后才返回成功响应。 - -## 9. 错误规范 - -| HTTP 状态码 | code | 说明 | -|-------------|------|------| -| 400 | 1001 | 请求 JSON 或字段结构错误 | -| 400 | 1006 | 请求大小超限 | -| 401 | 1101 | 机构码、用户名或密码错误 | -| 401 | 1102 | 访问令牌无效或已过期 | -| 404 | 1201 | 机构不存在 | -| 409 | 1202 | 机构已停用 | -| 500 | 1203 | 机构数据库未配置或配置无效 | -| 400 | 1301 | 数据源、目标或字段标识符格式错误 | -| 400 | 1302 | 过滤条件、排序或分页格式错误 | -| 400 | 1303 | 保存变更结构错误 | -| 400 | 1304 | SQL 为空、包含多条语句或不是只读查询 | -| 409 | 2001 | `expect_rows` 与实际影响行数不一致 | -| 409 | 2002 | 唯一键、外键、非空或检查约束冲突 | -| 409 | 2003 | 死锁、序列化失败或其他并发冲突 | -| 422 | 3001 | 数据源、目标或字段不存在 | -| 422 | 3002 | 数据值无法转换为数据库字段类型 | -| 500 | 3003 | 数据库执行失败 | -| 503 | 9001 | 机构数据库不可用 | -| 500 | 9002 | 服务端内部错误 | -| 504 | 9003 | 查询或保存执行超时 | - -错误响应不能泄露真实数据库名称、地址、端口、账号、密码或连接字符串。 - -保存请求执行失败时,错误响应的 `details` 应包含失败的 `change_index`。约束冲突可以返回约束名称,但不能返回生成后的完整 SQL。 - -## 10. 执行限制 - -默认限制: - -| 项目 | 限制 | -|------|------| -| 单次请求大小 | 2 MB | -| load 最大返回行数 | 5000 行 | -| load_sql 最大返回行数 | 5000 行 | -| load 最大过滤条件数 | 200 个 | -| save 最大变更数 | 100 个 | -| 单个 change 最大字段数 | 500 个 | -| load 执行超时 | 30 秒 | -| load_sql 执行超时 | 30 秒 | -| save 执行超时 | 60 秒 | - -规则: - -- 超过返回行数限制时请求失败,不静默截断结果。 -- save 超时包含全部变更和事务提交过程。 -- 限制值可以通过后端配置调整。 - -## 11. 当前范围 - -当前文档只定义: - -- 全部请求使用 `POST` 的通用规范。 -- 机构码传递和机构数据库路由规则。 -- 登录接口。 -- 登录响应中的用户信息和前端按钮权限。 -- 通用结构化查询接口。 -- 高级只读 SQL 查询接口。 -- 通用结构化保存接口。 -- 通用响应和基础错误码。 - -当前不提供原有的 `/api/query` 和 `/api/transaction`。数据访问统一使用 `/api/data/load`、`/api/data/load_sql` 和 `/api/data/save`。 - -文件、导入、导出、报表以及页面和菜单加载方式将在后续单独设计。 - -## 12. TypeScript 类型参考 - -```typescript -interface ApiResponse { - code: number - message: string - data?: T - details?: Record -} - -interface LoginRequest { - username: string - password: string -} - -interface LoginResult { - access_token: string - token_type: 'Bearer' - expires_in: number - organization: { - orgid: string - name: string - } - user: { - id: string | number - username: string - display_name: string - } - permissions: string[] -} - -type DataScalar = string | number | boolean | null -type DataValue = DataScalar | DataValue[] | { [key: string]: DataValue } - -type FilterNode = FilterCondition | FilterGroup - -interface FilterCondition { - field: string - op: - | 'eq' - | 'ne' - | 'gt' - | 'gte' - | 'lt' - | 'lte' - | 'in' - | 'not_in' - | 'between' - | 'contains' - | 'starts_with' - | 'ends_with' - | 'is_null' - | 'is_not_null' - value?: DataValue -} - -interface FilterGroup { - logic: 'and' | 'or' - items: FilterNode[] -} - -interface LoadRequest { - source: string - fields: string[] - filter?: FilterGroup - sort?: Array<{ - field: string - direction: 'asc' | 'desc' - }> - page?: { - page_no: number - page_size: number - } - include_total?: boolean -} - -interface LoadResult> { - rows: T[] - row_count: number - total: number | null - page_no: number - page_size: number -} - -interface LoadSqlRequest { - sql: string -} - -interface LoadSqlResult> { - rows: T[] - row_count: number -} - -type SaveAction = 'insert' | 'update' | 'delete' - -interface SaveChange { - action: SaveAction - target: string - values?: Record - filter?: FilterGroup - returning?: string[] - expect_rows?: number -} - -interface SaveRequest { - changes: SaveChange[] -} - -interface SaveResult> { - results: Array<{ - index: number - action: SaveAction - affected_rows: number - rows: T[] - }> -} -``` diff --git a/code/fms/软件需求.md b/code/fms/软件需求.md new file mode 100644 index 00000000..ddfda7bf --- /dev/null +++ b/code/fms/软件需求.md @@ -0,0 +1,188 @@ +# FMS 管理系统需求规范 + +## 设计原则 + +- FMS 是一个前后端分离的多机构管理系统。 +- 一个机构对应一个独立数据库,机构码统一命名为 `orgid`。 +- 登录时必须提交机构码、用户名和密码。后端根据机构码连接对应数据库并验证用户。 +- 登录成功后,登录凭证与机构码绑定。后续请求只操作当前登录机构的数据库,切换机构必须重新登录。 +- 后端只负责登录、机构数据库路由、通用数据接口、事务和少量特殊接口。 +- 页面、查询条件、SQL、数据组织、校验和普通业务流程均由前端完成。 +- 当前阶段不考虑安全性,前端可以直接拼接查询条件和 SQL。 +- 后端通用接口完成后,应尽量保持稳定。增加普通业务时只修改前端和数据库,不增加后端业务接口。 + +## 固定接口 + +| 方法 | 路径 | 用途 | +|------|------|------| +| `POST` | `/api/auth/login` | 机构登录 | +| `POST` | `/api/data/loaddata` | 按表或视图加载数据 | +| `POST` | `/api/data/loaddatabysql` | 执行完整查询 SQL | +| `POST` | `/api/data/page` | 执行分页查询 | +| `POST` | `/api/data/saveobjt` | 多表事务保存 | + +**登录** + +```json +{ + "orgid": "G3HD", + "username": "admin", + "password": "123456" +} +``` + +登录成功后返回登录凭证和用户信息。后续请求携带登录凭证,不再重复提交 `orgid`。 + +**loaddata** + +用于表或视图的普通查询,不执行分页。 + +```json +{ + "source": "v_bs_business", + "fields": ["b_id", "b_state", "customer_name"], + "where": "b_state = '草拟'", + "order_by": "b_id desc" +} +``` + +- `source` 必填,表示表名或视图名。 +- `fields` 可省略,省略时返回全部字段。 +- `where` 可省略,由前端直接拼接,不包含 `WHERE` 关键字。 +- `order_by` 可省略,不包含 `ORDER BY` 关键字。 + +成功响应: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "rows": [], + "row_count": 0 + } +} +``` + +**loaddatabysql** + +用于关联、聚合、统计等完整 SQL 查询,不执行分页。SQL 由前端直接拼接。 + +```json +{ + "sql": "select customer_id, sum(amount) as total from v_fee where fee_date >= '2026-01-01' group by customer_id" +} +``` + +成功响应与 `loaddata` 相同。 + +**page** + +所有分页查询统一使用本接口。 + +```json +{ + "sql": "select * from v_bs_business where b_state = '草拟'", + "order_by": "b_id desc", + "page_no": 1, + "page_size": 20 +} +``` + +- `sql` 必填,不包含分页语句。 +- `order_by` 必填,用于保证分页顺序稳定。 +- `page_no` 从 1 开始,默认值为 1。 +- `page_size` 默认值为 20。 +- 后端负责生成分页 SQL 和计算总记录数。 + +成功响应: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "rows": [], + "row_count": 0, + "total": 0, + "page_no": 1, + "page_size": 20 + } +} +``` + +**saveobjt** + +用于一个或多个表的新增、修改和删除。 + +```json +{ + "tables": [ + { + "table": "bs_business", + "key_field": "b_id", + "inserts": [], + "updates": [ + { + "b_id": "10001", + "b_state": "草拟" + } + ], + "deletes": [] + }, + { + "table": "bs_business_container", + "key_field": "subid", + "inserts": [], + "updates": [], + "deletes": [ + { + "subid": "20001" + } + ] + } + ] +} +``` + +- `tables` 必须是非空数组。 +- `table` 是目标表名,`key_field` 是该表的单个主键字段。 +- `inserts`、`updates` 和 `deletes` 均为可选数组,省略时按空数组处理。 +- 修改和删除的数据必须包含 `key_field` 对应的主键值。 +- 一次请求中的所有表和所有操作使用同一个数据库连接和事务。 +- 全部操作成功后提交;任意操作失败时全部回滚。 +- 保存成功只返回成功状态,不返回每张表的操作数量。 + +成功响应: + +```json +{ + "code": 0, + "message": "保存成功" +} +``` + +保存失败时可以返回失败位置: + +```json +{ + "code": 2001, + "message": "保存失败", + "details": { + "table": "bs_business_container", + "action": "update", + "index": 2 + } +} +``` + +## 统一约定 + +- API 路径使用全小写名称,JSON 多单词字段使用 `snake_case`。 +- `code = 0` 表示成功,非零值表示失败。 +- 查询结果统一放在 `data.rows` 中,没有数据时返回空数组。 +- 不使用参数占位符,查询条件和 SQL 均由前端拼接完成。 +- 不增加功能相同但名称不同的接口。 +- 不使用 `V2`、`V3`、`new`、`old`、`test` 或 `bak` 等临时名称。 +- 接口发生不兼容升级时,统一通过 `/api/v2` 等版本路径升级。 +- 文件、报表、导入、导出和其他通用接口无法完成的功能,可以单独定义特殊接口。