# 仓库物料管理系统 API 文档 **基础地址**:`/api` --- ## 阅读说明 ### 认证 除 `POST /auth/login` 外,所有接口都要求携带 JWT: | 请求头 | 值 | |----------------|-------------------| | Authorization | `Bearer ` | 缺少或失效的令牌返回 `401 Unauthorized`。标为「仅管理员」的接口由 `AdminOnly` 中间件拦截,非管理员返回 `403 Forbidden`。有效角色在每次请求时从数据库重载,JWT 中的旧角色快照不授予当前权限。 ### 公司上下文(2026-10-09) 组织级接口必须同时携带 `Authorization: Bearer ` 和 `X-Organization-ID: <正整数公司ID>`。适用范围为物料、分类、单位、编码规则、伙伴、仓库/库位、库存/预警/流水、入出库、申请/审批和物料日志。超级管理员也必须选择公司;没有跨公司汇总或默认公司兜底。 登录、当前用户、用户管理、组织管理、角色/权限管理、上传及下面的公司初始化/切换接口不要求组织头。公司授权只限制数据范围,功能角色权限仍单独校验。 | HTTP 状态 | 公司隔离语义 | | --- | --- | | 400 | 组织头缺失、不是纯数字正整数、请求参数非法,或引用另一公司的分类/单位/物料等 | | 403 | 当前公司不存在、停用或未授权,统一提示“当前公司不可访问”;功能权限不足也返回 403 | | 404 | 在已授权的公司上下文内查询、更新或删除另一公司的记录,或记录不存在 | | 409 | 仓库/库位缺失或停用导致库存写操作不可执行,或已有业务状态冲突 | 新建记录的归属取服务端验证的上下文,忽略请求体伪造的 `organization_id`。普通编辑不能迁移公司。列表、分页总数、搜索、编码查重、日志和状态变更均限制当前公司;不同公司允许相同物料编码。 ### 公司授权、初始化与切换 `GET /auth/organizations` 返回当前用户可访问的启用公司及其仓库摘要;普通用户按显式授权过滤,超级管理员得到所有启用公司: ```json { "code": 0, "message": "success", "data": { "organizations": [{ "id": 201, "code": "BEIJING", "name": "北京公司", "status": "active", "warehouse": {"id": 201, "name": "北京仓库", "status": "active", "location": {"id": 201, "name": "北京库位", "status": "active"}} }], "default_organization_id": 201 } } ``` `warehouse` 或 `location` 未配置时为 `null`;普通用户可读取摘要,无需仓库维护权限。`default_organization_id` 可为 `null`,不能据此隐式使用 ID 1。 `POST /auth/organization-context` 请求 `{"organization_id":202,"previous_organization_id":201}`,返回 `data: {organization_id, organization, warehouse}`。服务端独立验证目标公司并写入切换审计;审计失败则切换失败。该调用不改变用户的默认公司,也不保存全局会话公司,请求仍须携带组织头。 `POST /users` 和 `PUT /users/:id` 支持 `organization_ids` 与 `default_organization_id`。普通用户的默认公司必须属于启用授权列表且唯一;更新时省略授权字段表示保留原设置,显式空授权列表配合 `null` 默认公司表示清空。授权变更与审计在同一事务中提交。超级管理员无需逐公司授权。 首次登录选择有效默认公司;没有有效默认但有授权公司时要求选择,没有授权时禁止组织级请求。前端以每账号 `sessionStorage` 保存标签页选择,刷新重新校验。切换清理列表、详情、筛选、分页、弹窗及缓存,并以公司 ID 和上下文版本屏蔽旧响应/错误;深链接回对应列表。脏表单先确认放弃,保存期间禁止切换,登出/换账号清空状态。 ### 仓库、库存与单据事务 每个公司最多一个仓库,每个仓库最多一个库位,数据库唯一约束包括停用记录。页面只显示公司对应的仓库/库位摘要;服务端按公司解析库存维度,不信任客户端的仓库/库位归属。 | 配置状态 | 库存写操作提示(409) | | --- | --- | | 无仓库 | 请先配置本公司仓库 | | 仓库停用 | 本公司仓库已停用 | | 无库位 | 请先配置本公司仓库的库位 | | 库位停用 | 本公司库位已停用 | 上述状态允许维护主数据、查询本公司历史库存/单据/流水,以及创建申请、驳回、取消等不扣库存操作。入出库创建/修改/删除、预警修改及自动出库审批要求启用维度,禁止借用另一公司的资源。所有启用且授权公司均可使用已有业务模块。 入出库表头及库存流水保存 `organization_id/warehouse_id/location_id`。修改/删除在事务内锁定当前公司原单据,先按原库存维度恢复原单据造成的库存变化,再处理新的本公司明细;失败整体回滚。库存余额锁定完整维度与物料 ID,条件扣减防止透支,多物料按 ID 排序锁定。流水结存只在本公司内计算,提交成功后才发布本公司事件。 ### 申请与审批 `POST /applications` 在创建时绑定当前公司,明细只允许本公司有效物料;`GET /applications/my` 按公司和本人过滤,`GET /applications` 按公司及审批功能权限查询。审批、驳回和取消使用 `/applications/:id/approve`、`/reject`、`/cancel`;取消仍须本人,状态更新带公司、ID 和原状态条件。 审批请求支持 `{"auto_outbound":true}`。自动出库审批在同一事务中锁定本公司的 pending 申请,解析本公司启用仓库/库位,锁定并扣减库存,建立同公司出库单和流水,最后变更申请状态。重复审批不会重复出库;库存不足或中途故障回滚全部业务记录。非自动出库审批仍沿用现有库存检查规则。 ### WebSocket 公司订阅 `POST /auth/websocket-ticket` 必须携带令牌及组织头,返回 `data: {ticket, expires_at}`(到期 Unix 秒)。随机票据一次性、30 秒有效,绑定用户、公司和当前会话。使用 `/ws?ticket=<票据>` 握手,不在 URL 放长期 JWT。 握手和每次投递重新检查有效用户、公司授权及会话;撤销授权/角色、停用公司或登出后,失效连接不再接收事件。库存事件含 `type: "stock_update"`、`organization_id`、`action`、`order_id`,仅投递同公司订阅者。前端切换/登出关闭旧连接,重新取票建连,消息同时校验公司与上下文版本。 数据检查、升级顺序、验证证据和回退限制见 [公司隔离验收与交付](company-master-data-isolation-acceptance.md)。 ### 统一响应结构 所有接口共用同一个外层结构: | 字段 | 类型 | 说明 | |---------|---------|--------------------------------------------| | code | integer | `0` 表示成功;失败时等于 HTTP 状态码 | | message | string | 提示信息(后端硬编码,目前为英文) | | data | object | 业务数据;无返回值的接口为 `null` | ### 分页结构 列表接口的 `data` **不是数组**,而是一个分页对象: ```json { "code": 0, "message": "success", "data": { "items": [], "limit": 20, "offset": 0, "total": 0, "has_more": false } } ``` | 字段 | 类型 | 说明 | |----------|---------|------------------------------------------| | items | array | 当前页数据 | | limit | integer | 每页条数,默认 20,**上限 100** | | offset | integer | 偏移量,默认 0 | | total | integer | 符合条件的总条数 | | has_more | boolean | 是否还有下一页 | `limit` 传超过 100 的值会被服务端截断为 100,而不是报错。 ### 本文档覆盖范围 本文档详述**物料、入库、出库、库存**四个资源域,物料批量导入(`/material-import`)已并入 §1。以下分组已实现但未在本文档展开,需要时请直接查阅 `internal/handler/api.go` 的路由注册: | 分组 | 路径 | 权限 | |-------------------------|---------------------------------------------|--------| | 认证 | `POST /auth/login`、`GET /auth/me`、`POST /auth/logout` | 登录接口公开,其余需登录 | | 用户 | `/users` | 仅管理员 | | 组织 | `/organizations` | 仅管理员 | | 角色 / 权限 | `/roles`、`/permissions` | 仅管理员 | | 主数据:物料分类 | `/material-categories` | 仅管理员 | | 主数据:计量单位 | `/units` | 仅管理员 | | 主数据:编码规则 | `/code-rules`、`GET /code-rules/next` | 仅管理员;`/next` 需登录 | | 业务伙伴 | `/partners` | 仅管理员 | | 仓库与库位 | `/warehouses`、`/warehouses/:id/locations` | 仅管理员 | | 物料申请 | `/applications`、`/applications/my` | 需登录 | | 操作日志 | `/logs`、`/logs/materials/:materialId` | 需登录 | | 图片上传 | `POST /upload` | 需登录 | | 物料批量导入 | `GET /material-import/template`、`POST /material-import` | 需登录,见 §1 | --- ## 1. 物料接口 物料是主数据,**不承载任何库存信息**。库存数量和预警阈值分别由库存接口和入库流程维护。 ### GET /materials 分页查询物料列表。 **查询参数:** | 参数 | 类型 | 必填 | 说明 | |----------|---------|------|----------------------------| | search | string | 否 | 按物料编码或名称模糊搜索 | | status | string | 否 | `active`(默认)、`obsolete` 或 `all`;业务下拉应使用默认值,物料管理页使用 `all` | | limit | integer | 否 | 每页条数,默认 20,上限 100 | | offset | integer | 否 | 偏移量,默认 0 | > `status` 默认 `active`,所以该接口默认只返回有效物料,已作废的物料需要显式传 `status=all` 或 `status=obsolete` 才能查到。业务单据的物料下拉正是靠这个默认值自然排除了作废物料;库存流水的检索框则要显式传 `status=all`,才能查到已作废物料的历史。 **响应**(`data` 为分页对象,此处只展示 `items` 内元素): ```json { "code": 0, "message": "success", "data": { "items": [ { "id": 1, "organization_id": 1, "material_code": "A01-0001", "name": "内六角螺栓", "material_image": "/uploads/bolt.png", "spec": "M8x30", "manufacturer_model": "GB/T 70.1", "category_id": 12, "base_unit_id": 3, "unit": "个", "status": "active", "version": 1, "created_at": "2026-01-01T08:00:00Z", "updated_at": "2026-01-01T08:00:00Z" } ], "limit": 20, "offset": 0, "total": 1, "has_more": false } } ``` `material_image`、`spec`、`manufacturer_model`、`category_id`、`base_unit_id`、`unit` 为空时**不会出现在响应里**(后端 `omitempty`),前端取值时需要做空值兜底。 **状态码:** - `200 OK` — 成功 - `401 Unauthorized` — 未登录 --- ### GET /materials/:id 查询单个物料。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|----------| | id | integer | 是 | 物料 ID | **响应:** `data` 为单个物料对象,字段同 `GET /materials`。 **状态码:** - `200 OK` — 成功 - `400 Bad Request` — 物料 ID 非法 - `404 Not Found` — 物料不存在 --- ### POST /materials 创建物料。 **请求体:** ```json { "material_code": "A01-0001", "name": "内六角螺栓", "spec": "M8x30", "manufacturer_model": "GB/T 70.1", "category_id": 12, "base_unit_id": 3, "unit": "个", "material_image": "/uploads/bolt.png" } ``` | 字段 | 类型 | 必填 | 说明 | |-------------------|---------|------|--------------------------------------| | material_code | string | 是 | 物料编码,组织内唯一 | | name | string | 是 | 物料名称 | | category_id | integer | 是 | 小类分类 ID,必须大于 0 且真实存在 | | spec | string | 否 | 规格 | | manufacturer_model| string | 否 | 厂家型号 | | base_unit_id | integer | 否 | 基本计量单位 ID | | unit | string | 否 | 单位编码(冗余展示字段) | | material_image | string | 否 | 图片地址 | | organization_id | integer | 否 | 传入会被忽略,服务端取当前用户的组织 | **响应:** `201 Created`,`data` 为创建后的物料对象。 ```json { "code": 0, "message": "Material created successfully", "data": { "id": 1, "material_code": "A01-0001", "name": "内六角螺栓", "status": "active" } } ``` **状态码:** - `201 Created` — 创建成功 - `400 Bad Request` — 请求体非法、物料编码已存在,或 `category_id` 不是有效的小类类型 --- ### PUT /materials/:id 更新物料。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|----------| | id | integer | 是 | 物料 ID | **请求体:** ```json { "material_code": "A01-0001", "name": "内六角螺栓(镀锌)", "spec": "M8x35", "category_id": 12, "unit": "个" } ``` | 字段 | 类型 | 必填 | 说明 | |-------------------|---------|------|--------------------------| | category_id | integer | **是** | 即使不修改分类也必须传 | | material_code | string | 否 | 物料编码 | | name | string | 否 | 物料名称 | | spec | string | 否 | 规格 | | manufacturer_model| string | 否 | 厂家型号 | | base_unit_id | integer | 否 | 基本计量单位 ID | | unit | string | 否 | 单位编码 | | material_image | string | 否 | 图片地址 | | status | string | 否 | 物料状态 | > `category_id` 在更新接口里同样是必填项。只改名称而不传分类会返回 `400`。 **响应:** `200 OK`,`data` 为更新后的物料对象。 **状态码:** - `200 OK` — 更新成功 - `400 Bad Request` — 请求体非法或缺少 `category_id` - `404 Not Found` — 物料不存在 --- ### POST /materials/:id/obsolete 作废物料。这是**软删除**:物料从库存视图(`GET /stock`、`GET /stock/warning`)中消失,但记录保留,可以恢复。 > `GET /materials` 默认仅返回有效物料;物料管理页应传 `status=all` 才会同时看到作废项并执行撤销。入库、出库和申领的服务端也会拒绝使用已作废物料。历史单据仍可读取作废物料的名称与编码。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|----------| | id | integer | 是 | 物料 ID | **请求体:** 无。 **响应:** ```json { "code": 0, "message": "Material obsoleted successfully", "data": null } ``` **状态码:** - `200 OK` — 作废成功 - `400 Bad Request` — 物料 ID 非法 - `404 Not Found` — 物料不存在 - `409 Conflict` — **仍有库存(在库、预留、锁定或在途数量不为 0)**,需要先出库清零再作废 > 不再补货的物料走作废退役。作废要求库存为零,所以典型流程是:出库清零 → 作废。 > > 注意区分「作废」和「关掉预警」:作废是物料退出库存视野,不再出现在任何库存接口里;关掉预警(`warning_enabled: false`)只是不再提醒,物料和阈值都还在。另外,把阈值设成 0 不等于停用预警——0 是"库存降到 0 时预警",仍然会报警。 --- ### POST /materials/:id/restore 撤销售废,把物料恢复为可用状态。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|----------| | id | integer | 是 | 物料 ID | **请求体:** 无。 **响应:** ```json { "code": 0, "message": "Material restored successfully", "data": null } ``` **状态码:** - `200 OK` — 恢复成功 - `400 Bad Request` — 物料 ID 非法 - `404 Not Found` — 物料不存在 --- ### GET /material-import/template 下载批量导入用的 xlsx 模板。模板随代码实时生成、不落盘,所以下载到的列定义永远与当前校验规则一致。 **请求:** 无参数。 **响应:** xlsx 文件流,`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,文件名为 `物料导入模板.xlsx`。 模板含两张工作表,按以下顺序排列,打开时默认显示「填写说明」: | 工作表 | 内容 | |--------|------| | 填写说明 | 字段规则、填写与上传步骤、容量限制、整批校验、重复导入提醒和参考示例 | | 物料数据 | 第 1 行是冻结表头,从第 2 行填写;五列采用文本格式,数据页不放示例物料 | `物料数据` 的表头是 5 列:`物料名称`、`规格`、`单位`、`大类`、`小类`。解析时**按列名定位,不按列位置**: - 列顺序可以随意调整;表头允许带首尾空格和末尾的 `*` 必填标记;不认识的列忽略; - 必填列 `物料名称`、`大类`、`小类` 缺任何一列,判为模板不合法并返回「模板格式不正确,请重新下载最新模板」; - 选填列 `规格`、`单位` 可以整列删掉,取值按空处理。 分类**按名称**填写;名称之外也接受**编码**和界面上的「编码 名称」写法(界面一律显示成 `A 机械结构类`,用户整段照抄进模板是常事)。后端按「大类 + 小类」在本公司内组合匹配(小类名在一个公司内会重名,例如多个大类下都有「其他」,所以必须两级组合)。名称、编码、「编码 名称」三种写法同名或同码时以**名称**优先。单位按名称匹配,同时兼容按编码匹配(如 `kg` 与 `千克`)。 ### POST /material-import 上传填好的 xlsx,**校验通过即在同一个请求内入库**——没有「预览确认」环节,也没有暂存任务 ID。 **请求:** `multipart/form-data`,文件字段名 `file`。仅支持 `.xlsx`,单文件不超过 5 MB,数据区不超过 20,000 行(不含表头)。 服务端在 multipart 解析前限制请求体为 5 MiB + 64 KiB(表单开销),文件本身仍限制为 5 MiB;超大请求在读到上限时提前拒绝。Excel 使用逐行迭代读取,发现第 20,001 条非空数据时立即停止,表头和空行不计数;较大的工作表 XML 使用临时文件,避免将整张表的所有行缓存到内存。 **逐行校验规则:** | 列 | 规则 | |----|------| | 物料名称 | 必填,去首尾空格后非空,≤ 200 字 | | 规格 | 选填,≤ 200 字 | | 单位 | 选填;留空时默认「个」。非空时必须在当前公司已启用单位中按名称或编码匹配到 | | 大类 | 必填,必须是当前公司已启用的顶层分类,按名称、编码或「编码 名称」匹配 | | 小类 | 必填,必须是该大类下已启用的小类,同样认名称、编码或「编码 名称」 | | 编码规则 | 该小类必须存在启用的编码规则(`code_rules.status = 'active'`) | 物料编码由系统按小类编码规则生成,**不接受模板里的编码列**。单位落库统一写该单位的**编码**——模板里填名称(如「千克」)时也会转成编码,与单条新增保持一致。同一行可能产生多条原因,用中文分号连接。 **成功响应 `201 Created`:** ```json { "code": 0, "message": "成功导入 200 条物料", "data": { "total_rows": 200, "success_rows": 200, "failed_rows": 0, "errors": [] } } ``` **校验失败响应 `400 Bad Request`**(HTTP 状态是 400,但 `data` 里带完整明细): ```json { "code": 400, "message": "有 3 行校验未通过,本次未导入任何数据", "data": { "total_rows": 200, "success_rows": 0, "failed_rows": 3, "errors": [ { "row": 3, "data": { "name": "", "spec": "M8x30", "unit": "个", "major": "机械结构类", "minor": "标准件" }, "reason": "物料名称不能为空" } ] } } ``` `row` 是 **Excel 里的真实行号**(从 1 开始、含表头行),用户可以直接对着 Excel 定位;`data` 是该行的原始内容。 **状态码:** - `201 Created` — 全部行通过并入库 - `400 Bad Request` — 存在校验未通过的行(整批不入库);模板不合法(「模板格式不正确,请重新下载最新模板」)、行数超限(「导入行数超过 20000 行上限」)、未选文件(「请选择要导入的 xlsx 文件」)、文件超过 5 MB(「文件超过 5MB 上限」) - `500 Internal Server Error` — 入库失败,事务已回滚,整批未落库 **行为说明:** - **任意一行校验失败,整批不入库**,响应中返回全部逐行错误;重新上传前请先修正后重试。 - 整批在**一个事务**内完成:按小类锁编码规则 → 分配编码 → 分批插入物料(每批 500 行)→ 写审计日志;任一步失败整体回滚。 - **仅记录成功导入**:事务提交后写一条 `material_import_logs` 成功汇总,包含操作人、公司、时间、文件名、总行数和成功数,失败数为 0、状态为 `success`、失败原因为空;失败导入不保存历史记录。**逐行错误明细只随响应返回,不落库。** 成功新增的每件物料仍写 CREATE 日志及完整 `new_value` JSON 快照;现有日志页「查看详情」显示「批量导入创建:物料名称(物料编码)」,下方按行展示导入时的编码、名称、规格、分类、单位和状态;该详情是物料操作日志,不展示导入文件名或批次汇总。 - 首版只支持**新增**物料:不做更新,也不自动创建分类或单位。 - **不做重复检测**:同一份文件重复上传会真的导入两遍(生成不同的编码)。 --- ## 2. 入库接口 入库是物料**进入库存视野**的唯一入口:物料的第一行库存记录和第一个预警阈值都在这里产生。入库成功后,服务端会通过 WebSocket 广播库存变更。 ### GET /inbound 分页查询入库单列表。 **查询参数:** | 参数 | 类型 | 必填 | 说明 | |--------|---------|------|----------------------------| | search | string | 否 | 按经手人或供应商模糊搜索 | | limit | integer | 否 | 每页条数,默认 20,上限 100 | | offset | integer | 否 | 偏移量,默认 0 | **响应**(`data` 为分页对象,此处只展示 `items` 内元素): ```json { "code": 0, "message": "success", "data": { "items": [ { "id": 1, "inbound_date": "2026-01-01T08:00:00Z", "handler": "张三", "supplier": "ABC 钢材", "status": "completed", "created_at": "2026-01-01T08:00:00Z", "updated_at": "2026-01-01T08:00:00Z" } ], "limit": 20, "offset": 0, "total": 1, "has_more": false } } ``` > 单据的 `status` 字段目前是服务端**硬编码的固定值 `"completed"`**(数据库里没有对应的状态列),所有入库单都返回同一个值,不能用于区分单据阶段。出库单同理。 **状态码:** `200 OK`、`401 Unauthorized` --- ### GET /inbound/:id 查询单个入库单,**包含明细行**。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|------------| | id | integer | 是 | 入库单 ID | **响应:** ```json { "code": 0, "message": "success", "data": { "id": 1, "inbound_date": "2026-01-01T08:00:00Z", "handler": "张三", "supplier": "ABC 钢材", "status": "completed", "created_at": "2026-01-01T08:00:00Z", "updated_at": "2026-01-01T08:00:00Z", "items": [ { "id": 1, "inbound_order_id": 1, "material_id": 1, "material_code": "A01-0001", "material_name": "内六角螺栓", "quantity": 500 } ] } } ``` **状态码:** - `200 OK` — 成功 - `400 Bad Request` — 入库单 ID 非法 - `404 Not Found` — 入库单不存在 --- ### POST /inbound 创建入库单。创建时库存立即增加,同一个事务内写入预警阈值。 **请求体:** ```json { "handler": "张三", "supplier": "ABC 钢材", "remark": "到货验收通过", "items": [ { "material_id": 1, "quantity": 500, "warning_stock": 100, "warning_enabled": true }, { "material_id": 2, "quantity": 200, "warning_stock": 0, "warning_enabled": false }, { "material_id": 3, "quantity": 80 } ] } ``` | 字段 | 类型 | 必填 | 说明 | |----------|--------|------|--------------------| | handler | string | 是 | 经手人 | | supplier | string | 是 | 供应商 | | remark | string | 否 | 备注 | | items | array | 是 | 明细,至少 1 行 | **明细字段:** | 字段 | 类型 | 必填 | 说明 | |-----------------|---------|------|------------------------------------------------------------| | material_id | integer | 是 | 物料 ID | | quantity | integer | 是 | 入库数量,必须大于 0 | | warning_stock | number | 否 | 为该物料设置的预警阈值,`0` 表示"库存降到 0 时预警";**不传表示不改动已存的阈值** | | warning_enabled | boolean | 否 | 预警是否启用;**不传默认启用**,传 `false` 关闭预警。仅在同时传了 `warning_stock` 时生效 | > `warning_stock` 与库存更新在同一个事务里落库,保证物料的第一个阈值和第一行库存同时生效。编辑旧入库单时**不传**这两个字段(而不是传 `0` 或 `false`),才不会把别处刚调整好的配置覆盖回去。 > > 关闭预警而想保留原阈值时,把该物料当前的阈值连同 `warning_enabled: false` 一起传回即可(前端表单正是这么做的)。 > `inbound_no` 由服务端按 `IN-` 加 8 位订单 ID 自动生成;`operator` 为创建该单据的当前登录用户。二者均由服务端维护,不能由请求体指定。 **响应:** `201 Created`,`data` 为创建后的入库单,结构同 `GET /inbound/:id`。 **状态码:** - `201 Created` — 创建成功 - `400 Bad Request` — 请求体非法、物料不存在,或 `warning_stock` 为负数 --- ### PUT /inbound/:id 更新入库单。服务端会先恢复原明细造成的库存变化,再按新明细重新入库,两步在同一事务内完成。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|----------| | id | integer | 是 | 入库单 ID | **请求体:** 字段同 `POST /inbound`。`warning_stock` 和 `warning_enabled` 同样遵循「不传即不改动」的语义。 **响应:** `200 OK`,`data` 为更新后的入库单。 **状态码:** - `200 OK` — 更新成功 - `400 Bad Request` — 请求体非法、物料不存在,或 `warning_stock` 为负数 - `500 Internal Server Error` — 入库单不存在等错误 --- ### DELETE /inbound/:id 删除入库单。同时**恢复该单增加的库存**并删除对应的库存流水。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|----------| | id | integer | 是 | 入库单 ID | **响应:** ```json { "code": 0, "message": "Inbound order deleted successfully", "data": null } ``` **状态码:** - `200 OK` — 删除成功 - `400 Bad Request` — 入库单 ID 非法 - `500 Internal Server Error` — 入库单不存在等错误 --- ### GET /inbound/materials/:materialId 查询某个物料的入库记录。**该接口不分页**,`data` 直接是数组。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------------|---------|------|----------| | materialId | integer | 是 | 物料 ID | **响应:** ```json { "code": 0, "message": "success", "data": [ { "id": 1, "inbound_id": 1, "handler": "张三", "supplier": "ABC 钢材", "quantity": 500, "created_at": "2026-01-01T08:00:00Z" } ] } ``` **状态码:** `200 OK`、`400 Bad Request` --- ## 3. 出库接口 出库会校验可用库存,**库存不足时整单失败**。出库成功后同样会广播 WebSocket 库存变更。 ### GET /outbound 分页查询出库单列表。 **查询参数:** | 参数 | 类型 | 必填 | 说明 | |--------|---------|------|----------------------------| | search | string | 否 | 按经手人或领用人模糊搜索 | | limit | integer | 否 | 每页条数,默认 20,上限 100 | | offset | integer | 否 | 偏移量,默认 0 | **响应**(`data` 为分页对象,此处只展示 `items` 内元素): ```json { "code": 0, "message": "success", "data": { "items": [ { "id": 1, "outbound_date": "2026-01-01T08:00:00Z", "receiver": "装配车间", "status": "completed", "created_at": "2026-01-01T08:00:00Z", "updated_at": "2026-01-01T08:00:00Z" } ], "limit": 20, "offset": 0, "total": 1, "has_more": false } } ``` **状态码:** `200 OK`、`401 Unauthorized` --- ### GET /outbound/:id 查询单个出库单,**包含明细行**。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|------------| | id | integer | 是 | 出库单 ID | **响应:** ```json { "code": 0, "message": "success", "data": { "id": 1, "outbound_date": "2026-01-01T08:00:00Z", "handler": "李四", "receiver": "装配车间", "status": "completed", "created_at": "2026-01-01T08:00:00Z", "updated_at": "2026-01-01T08:00:00Z", "items": [ { "id": 1, "outbound_order_id": 1, "material_id": 1, "material_code": "A01-0001", "material_name": "内六角螺栓", "quantity": 100 } ] } } ``` **状态码:** - `200 OK` — 成功 - `400 Bad Request` — 出库单 ID 非法 - `404 Not Found` — 出库单不存在 --- ### POST /outbound 创建出库单。库存立即扣减,库存不足时整单失败。 **请求体:** ```json { "handler": "李四", "receiver": "装配车间", "remark": "急用", "items": [ { "material_id": 1, "quantity": 100 }, { "material_id": 2, "quantity": 50 } ] } ``` | 字段 | 类型 | 必填 | 说明 | |----------|--------|------|------------------| | receiver | string | 是 | 领用人 | | remark | string | 否 | 备注 | | items | array | 是 | 明细,至少 1 行 | **明细字段:** | 字段 | 类型 | 必填 | 说明 | |-------------|---------|------|----------------------| | material_id | integer | 是 | 物料 ID | | quantity | integer | 是 | 出库数量,必须大于 0 | `outbound_no` 由服务端按 `OUT-` 加 8 位订单 ID 自动生成;`operator` 与 `handler` 均由服务端设置为当前登录用户名,不能由请求体指定。 **响应:** `201 Created`,`data` 为创建后的出库单,结构同 `GET /outbound/:id`。 **状态码:** - `201 Created` — 创建成功 - `400 Bad Request` — 请求体非法、物料不存在,或**库存不足** --- ### PUT /outbound/:id 更新出库单。服务端先恢复原明细扣减的库存,再按新明细重新扣减,两步在同一事务内完成。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|------------| | id | integer | 是 | 出库单 ID | **请求体:** 字段同 `POST /outbound`。 **响应:** `200 OK`,`data` 为更新后的出库单。 **状态码:** - `200 OK` — 更新成功 - `400 Bad Request` — 请求体非法、物料不存在,或库存不足 - `500 Internal Server Error` — 出库单不存在等错误 --- ### DELETE /outbound/:id 删除出库单。同时**把已扣减的库存加回来**并删除对应的库存流水。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------|---------|------|------------| | id | integer | 是 | 出库单 ID | **响应:** ```json { "code": 0, "message": "Outbound order deleted successfully", "data": null } ``` **状态码:** - `200 OK` — 删除成功 - `400 Bad Request` — 出库单 ID 非法 - `500 Internal Server Error` — 出库单不存在等错误 --- ### GET /outbound/materials/:materialId 查询某个物料的出库记录。**该接口不分页**,`data` 直接是数组。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------------|---------|------|----------| | materialId | integer | 是 | 物料 ID | **响应:** ```json { "code": 0, "message": "success", "data": [ { "id": 1, "outbound_id": 1, "handler": "李四", "receiver": "装配车间", "quantity": 100, "created_at": "2026-01-01T08:00:00Z" } ] } ``` **状态码:** `200 OK`、`400 Bad Request` --- ## 4. 库存接口 ### 库存状态的判定规则 整套预警逻辑只有一句话: ``` 参与预警 = 启用 报警 = 参与预警 AND 现有库存 <= 阈值 ``` 展开到接口行为上: - **库存查询(`GET /stock`)只列「入过库」的物料**。判断依据是存在库存记录行,而不是库存数量大于 0——所以**已被领用到 0 的物料仍然会列出**,而只存在于主数据、从未入库的物料不会出现。已作废的物料也不列出。 - **阈值为 0 是合法配置**,含义是"库存降到 0 时预警"。 - **「不设置预警」等同于「不启用预警」**,两者是同一个状态。从没配过规则和手动关闭预警的物料,接口统一返回 `warning_enabled: false`,都不报警。需要区分这两者做表单预填时看 `warning_configured`(它只表示"配没配过规则",不参与判定)。 - **关闭预警时阈值本身保留**,随时可以重新启用。 | 是否入过库 | 是否配过规则 | 现有库存 | 阈值 | 启用 | 状态 | |------------|--------------|----------|------|------|----------| | 否 | — | 0 | — | — | 不在列表中 | | 是 | 否 | 0 | — | false | 正常 | | 是 | 是 | 5 | 0 | 是 | 正常 | | 是 | 是 | 0 | 0 | 是 | 库存不足 | | 是 | 是 | 0 | 10 | 是 | 库存不足 | | 是 | 是 | 10 | 10 | 是 | 库存不足 | | 是 | 是 | 15 | 10 | 是 | 正常 | | 是 | 是 | 0 | 10 | 否 | 正常 | ### GET /stock 分页查询库存,只包含入过库且未作废的物料。 **查询参数:** | 参数 | 类型 | 必填 | 说明 | |--------|---------|------|----------------------------| | search | string | 否 | 按物料编码或名称模糊搜索 | | limit | integer | 否 | 每页条数,默认 20,上限 100 | | offset | integer | 否 | 偏移量,默认 0 | **响应**(`data` 为分页对象,此处只展示 `items` 内元素): ```json { "code": 0, "message": "success", "data": { "items": [ { "material_id": 1, "material_code": "A01-0001", "name": "内六角螺栓", "spec": "M8x30", "unit": "个", "major_category": "A01", "minor_category": "A01-01", "total_on_hand": "500", "warning_stock": "100", "warning_enabled": true, "warning_configured": true, "stock_status": "normal" } ], "limit": 20, "offset": 0, "total": 1, "has_more": false } } ``` | 字段 | 类型 | 说明 | |--------------------|---------|------------------------------------------------------------| | major_category | string | 大类分类编码 | | minor_category | string | 小类分类编码 | | total_on_hand | string | 现有库存合计,以字符串返回以免精度丢失 | | warning_stock | string | 预警阈值,`"0"` 表示"库存降到 0 时预警" | | warning_enabled | boolean | 预警是否启用。没配过规则和手动关闭都是 `false` | | warning_configured | boolean | 是否配过预警规则。仅供表单预填区分"从没配过"和"已关闭",不参与判定 | | stock_status | string | `low`(库存不足)或 `normal`(正常) | **状态码:** `200 OK`、`401 Unauthorized` --- ### GET /stock/:materialId 查询单个物料的库存。与 `GET /stock` 不同,**这个接口对任何物料都会给出答案**,包括从未入库的物料(返回 `total_on_hand` 为 0)。物料申请和出库表单依赖这个特性逐个查询物料。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------------|---------|------|----------| | materialId | integer | 是 | 物料 ID | **响应:** ```json { "code": 0, "message": "success", "data": { "material_id": 1, "material_code": "A01-0001", "name": "内六角螺栓", "spec": "M8x30", "unit": "个", "major_category": "A01", "minor_category": "A01-01", "total_on_hand": "500", "warning_stock": "100", "warning_enabled": true, "warning_configured": true, "stock_status": "normal" } } ``` > `PUT /stock/:materialId/warning` 的入库表单预填依赖 `warning_configured`:没配过规则的物料要按"默认启用 + 阈值 0"预填,配过但被关闭的物料必须保持关闭,否则入库会把别人刚关掉的预警悄悄重新打开。 **状态码:** - `200 OK` — 成功 - `400 Bad Request` — 物料 ID 非法 - `404 Not Found` — 物料不存在 --- ### GET /stock/:materialId/details 查询某个物料在各库存维度(组织 / 仓库 / 库位)上的明细。**该接口不分页**,`data` 直接是数组;物料存在但从没有过库存记录时返回空数组。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------------|---------|------|----------| | materialId | integer | 是 | 物料 ID | **响应:** ```json { "code": 0, "message": "success", "data": [ { "id": 1, "organization_id": 1, "organization": "默认组织", "warehouse_id": 1, "warehouse": "默认仓库", "location_id": 1, "location": "主库位", "material_id": 1, "on_hand": "500", "reserved": "0", "locked": "0", "in_transit": "0", "updated_at": "2026-01-01T08:00:00Z" } ] } ``` | 字段 | 类型 | 说明 | |------------|---------|------------------------| | on_hand | string | 现有库存 | | reserved | string | 预留数量 | | locked | string | 锁定数量 | | in_transit | string | 在途数量 | > `reserved`、`locked`、`in_transit` 三个维度当前没有任何业务写入,恒为 `0`;只有 `on_hand` 会随出入库变动。作废校验会把这四项一起纳入判断。 **状态码:** - `200 OK` — 成功 - `400 Bad Request` — 物料 ID 非法 - `404 Not Found` — 物料不存在 --- ### GET /stock/warning 分页查询需要关注的物料:预警处于启用状态,并且现有库存已触达阈值。阈值为 0 的物料在库存降到 0 时也会出现在这里。 **查询参数:** `limit`(默认 20,上限 100)、`offset`(默认 0)。 **响应**(`data` 为分页对象,此处只展示 `items` 内元素): ```json { "code": 0, "message": "success", "data": { "items": [ { "material_id": 2, "material_code": "A02-0003", "name": "铝棒", "spec": "20mm x 3000mm", "unit": "根", "major_category": "A02", "minor_category": "A02-01", "total_on_hand": "30", "warning_stock": "100", "warning_enabled": true, "warning_configured": true, "stock_status": "low" } ], "limit": 20, "offset": 0, "total": 1, "has_more": false } } ``` **状态码:** `200 OK`、`401 Unauthorized` --- ### PUT /stock/:materialId/warning 设置、调整或停用某个物料的预警阈值。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------------|---------|------|----------| | materialId | integer | 是 | 物料 ID | **请求体:** ```json { "warning_stock": 100, "enabled": true } ``` | 字段 | 类型 | 必填 | 说明 | |---------------|---------|------|------------------------------------------------------------| | warning_stock | number | 是 | 阈值,必须大于等于 0。`0` 是合法值,表示"库存降到 0 时预警" | | enabled | boolean | 否 | 默认 `true`;传 `false` 停用该物料的预警,阈值本身保留 | **响应:** ```json { "code": 0, "message": "Stock warning updated successfully", "data": null } ``` **状态码:** - `200 OK` — 成功 - `400 Bad Request` — 物料 ID 非法,或 `warning_stock` 为负数 --- ### GET /stock/logs 分页查询全部库存流水,按时间倒序。 **查询参数:** `limit`(默认 20,上限 100)、`offset`(默认 0)。 **响应**(`data` 为分页对象,此处只展示 `items` 内元素): ```json { "code": 0, "message": "success", "data": { "items": [ { "id": 2, "material_id": 1, "change_type": "outbound", "change_quantity": "-100", "before_quantity": "500", "after_quantity": "400", "reference_id": 1, "reference_type": "outbound", "created_at": "2026-01-01T10:00:00Z" } ], "limit": 20, "offset": 0, "total": 1, "has_more": false } } ``` | 字段 | 类型 | 说明 | |-----------------|--------|------------------------------------------------| | change_type | string | `inbound`(入库)或 `outbound`(出库) | | change_quantity | string | **带符号**的变动数量,出库为负 | | before_quantity | string | 变动前库存 | | after_quantity | string | 变动后库存 | | reference_type | string | 来源单据类型,如 `inbound`、`outbound` | | reference_id | integer| 来源单据 ID | **状态码:** `200 OK`、`401 Unauthorized` --- ### GET /stock/logs/:materialId 分页查询某个物料的库存流水。 **路径参数:** | 参数 | 类型 | 必填 | 说明 | |------------|---------|------|----------| | materialId | integer | 是 | 物料 ID | **查询参数:** `limit`(默认 20,上限 100)、`offset`(默认 0)。 **响应**:结构与 `GET /stock/logs` 相同。`change_quantity` 采用带符号约定:入库为正数、出库为负数;`change_type` 仍用于展示入库/出库文字、样式和筛选。 **状态码:** - `200 OK` — 成功 - `404 Not Found` — 物料不存在 --- ## 状态码汇总 | 状态码 | 说明 | |--------|--------------------------------------------------| | 200 | 请求成功 | | 201 | 资源创建成功 | | 400 | 请求体或参数非法、物料不存在、库存不足 | | 401 | 未登录或令牌失效 | | 403 | 已登录但权限不足(如非管理员访问管理员接口) | | 404 | 资源不存在 | | 409 | 状态冲突:物料仍有库存时作废,或本公司仓库/库位缺失、停用而禁止库存写操作 | | 500 | 服务端错误 |