# 打包负责人物资申领

> 负责人提交（含批量）/撤销申领，仓管分批出库、手动完成、拒绝；状态机、按钮显隐字段、出库记录，以及进销存出入库权限与库房库存口径

- 类型：前端对接
- 分类：仓库配货 / 打包
- 端：梦花仓管小程序
- 后端服务：gallery
- 更新：2026-11-04
- 网页版：https://doc.xfun.fun/wms-material-apply-api

梦花仓管小程序 · 进销存

后端模块 `xm-bulky-mall-gallery`（与 `/supplier/wms/inventory/*` 同一服务）。

## 通用约定

跟 `/supplier/wms/inventory/*` 一样：base `https://api.xunmengvip.cn/xm-bulky-mall-gallery`，全部 POST + JSON body，鉴权、`{ code, msg, data, success }` 包装（`success` 是布尔，等价于 `code === 0`；失败时 HTTP 状态码仍是 200，别用状态码判断成败）、`index` / `size` 分页都照旧。失败响应的 `msg` 是可以直接 toast 的中文，速查见「报错文案速查」。

> [!NOTE]
> **入参怪癖**
>
> `/apply/sku/stock`、`/apply/detail`、`/apply/cancel`、`/apply/finish` 复用通用的 `IdQo`，入参字段名都叫 `id`。其中 **`/apply/sku/stock` 的 `id` 传的是 skuId，不是申领单 ID**，其余才是申领单 ID。

## 角色与状态机

后端从两处判定当前账号的身份，不需要前端传角色来鉴权：

| 角色 | 判定依据 | 能做什么 |
|----|----|----|
| **仓库管理员** | 账号命中配置的仓管角色 | 看全部申领单、出库、手动完成、拒绝。**不能提交申领**（除非同时也是负责人） |
| **打包负责人** | `xm_bulky_team.leader_id` = 当前账号，且团队为启用状态 | 提交申领、撤销自己提交且未出库的单、只看自己带的大区的单 |
| 两者都不是 | — | 调任意申领接口都会收到 `无物资申领权限` |

一个账号可以同时是仓管和负责人，这种情况下列表默认看全部；前端有「角色切换」时，把当前视角作为 `roleName` 传给 `/apply/page`（见「POST /supplier/wms/apply/page」）。

### 申领单状态

| status | 名称 | 说明 |
|----|----|----|
| 10 | 待审核 | 负责人提交后的初始态。此时 **库存不动**，不占用、不冻结 |
| 20 | 审核通过 | 历史数据状态，新单据不会再进入该状态 |
| 30 | 已拒绝 | 仓管拒绝，`rejectReason` 可能为空字符串。仅未出库的待审核单可拒 |
| 40 | 已撤销 | 申领人本人撤回，仅未出库的待审核单可撤 |
| 50 | 部分申领 | 已出库但累计出库数量 \< 申领数量，可继续出库或手动完成 |
| 60 | 已完成 | 累计出库 = 申领数量时自动完成，或仓管对部分申领单手动完成 |

```text
10 待审核 ──出库(未领满)──▶ 50 部分申领 ──出库(领满)──▶ 60 已完成（completeType=1 自动）
   │   └──出库(一次领满)─────────────────────────────▶ 60 已完成（completeType=1 自动）
   │                       50 部分申领 ──手动完成────▶ 60 已完成（completeType=2 手动）
   ├──拒绝──▶ 30 已拒绝
   └──撤销──▶ 40 已撤销
```

`completeType`：0 未完成、1 自动完成、2 手动完成、3 历史完成。

> [!NOTE]
> **待审核不占库存**
>
> 提交时只校验一次「申领数量 ≤ 库房剩余可用」，之后不做任何预占。所以两张待审核的单可能都显示能发，实际出库时第二张可能因库房库存不够失败。**出库失败的 toast 要能正常展示，不要假设提交通过就一定能足量出库。**

> [!NOTE]
> **手动完成不补数也不退库**
>
> 手动完成只是关闭单据：`issuedQuantity`、`remainingQuantity`、`progressPercent` 保持完成时的值，不再允许出库，也不调整任何库存。

## 负责人侧

### POST /supplier/wms/apply/sku/stock · 查询物资库房剩余可用库存

调用方：负责人 / 仓管

选好物资后调用，用于在申领数量输入框旁边展示「当前可申领上限」，也可用于出库弹窗展示库房可发数量。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | **物资 SKU ID**（不是申领单 ID），取自 `/supplier/wms/inventory/sku/list` 的 `skuId` |

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `skuId` | Long | 物资 SKU ID |
| `skuName` | String | 物资名称（即「物资类别」） |
| `specification` | String | 物资规格 |
| `unitName` | String | 计量单位 |
| `availableStock` | Integer | 库房剩余可用 = 全局实物 − 各大区已分配剩余 |

#### 响应示例

```json
{
  "skuId": 1012,
  "skuName": "打包纸箱",
  "specification": "60×40×40",
  "unitName": "个",
  "availableStock": 320
}
```

### POST /supplier/wms/apply/create · 提交物资申领

调用方：仅负责人

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `skuId` | Long | 是 | 物资 SKU ID |
| `teamId` | Integer | 否 | 申领大区。**负责人只带 1 个大区时可以不传**；带多个大区必传，不传报「请选择申领大区」。传了非自己带的大区报「无权为该大区申领」 |
| `quantity` | Integer | 是 | 申领数量，≥ 1 且不能超过 `availableStock` |
| `voucherUrls` | String | 否 | 凭证图片，**JSON 数组的字符串**，不是数组对象。最多 5 个元素，整串 ≤ 512 字符；不传或空串表示无凭证 |
| `remark` | String | 否 | 备注，≤ 255 字符 |

> [!IMPORTANT]
> **voucherUrls 是字符串，不是数组**
>
> 后端字段类型是 `String`，前端要自己 `JSON.stringify` 后再放进 body。**512 字符是整串长度**，包含引号和逗号，OSS 地址长的话 5 张可能放不下，提交前先量一次 `voucherUrls.length`。

#### 请求示例

```json
{ "skuId": 1012, "teamId": 3, "quantity": 50, "voucherUrls": "[\"https://oss/a.jpg\",\"https://oss/b.jpg\"]", "remark": "本周加班" }
```

传 `"voucherUrls": ["https://oss/a.jpg"]`（数组对象）会报「凭证图片格式错误」。

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `id` | Long | 新建申领单 ID，可直接跳详情或生成二维码 |
| `applyNo` | String | 申领单号，`SL` + yyyyMMdd + 3 位序列 |
| `skuId` | Long | 申领物资 SKU ID |

#### 响应示例

```json
{ "id": 88, "applyNo": "SL20261104001", "skuId": 1012 }
```

### POST /supplier/wms/apply/batchCreate · 批量提交物资申领

调用方：仅负责人

同一物资类别（`skuName` 相同）下一次申领多个规格，**每个规格各生成一张申领单**。整批在一个事务里，任一规格校验失败整批不提交。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `items` | Array | 是 | 规格明细，1 ~ 100 项，`skuId` 不能重复，且必须属于同一物资类别 |
| `items[].skuId` | Long | 是 | 物资 SKU ID |
| `items[].quantity` | Integer | 是 | 申领数量，≥ 1 且不能超过该规格的 `availableStock` |
| `teamId` | Integer | 否 | 申领大区，规则同「POST /supplier/wms/apply/create」 |
| `voucherUrls` | String | 否 | 本批共用的凭证图片，格式同单个提交 |
| `remark` | String | 否 | 本批共用的备注，≤ 255 字符 |

#### 请求示例

```json
{
  "teamId": 3,
  "items": [
    { "skuId": 1012, "quantity": 50 },
    { "skuId": 1013, "quantity": 20 }
  ],
  "remark": "本周加班"
}
```

#### 响应

`data` 为数组，每项字段同「POST /supplier/wms/apply/create」的响应（`id` / `applyNo` / `skuId`），顺序按 `skuId` 升序。

### POST /supplier/wms/apply/cancel · 撤销物资申领

调用方：仅申领人本人

入参 `{ "id": 申领单ID }`，响应 `data` 为 `null`。

只有 **申领人本人**、单据处于 **待审核**且 **尚未出库**时才能撤销，否则返回 `仅申请人可以撤销尚未出库的待审核申领单`。并发下状态已被别人改掉时返回 `申领单状态已变更，请刷新后重试`，收到后刷新列表。

## 列表与详情

### POST /supplier/wms/apply/page · 申领列表

调用方：负责人 / 仓管

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |
| `status` | Integer | 否 | 10/20/30/40/50/60，不传查全部。传其他值报「申领状态不合法」 |
| `skuId` | Long | 否 | 物资 SKU ID |
| `teamId` | Integer | 否 | 申领大区 ID |
| `startTime` | String | 否 | 申请开始时间，`yyyy-MM-dd HH:mm:ss`，按申领时间 ≥ 过滤 |
| `endTime` | String | 否 | 申请结束时间，`yyyy-MM-dd HH:mm:ss`，按申领时间 ≤ 过滤；早于 `startTime` 报「开始时间不能晚于结束时间」 |
| `roleName` | String | 否 | 当前视角角色名。传 `"打包负责人"` 时只看自己带的大区 |

> [!NOTE]
> **roleName 的作用范围**
>
> - **非仓管账号**：无论传不传 `roleName`，永远只能看到自己带的大区的单。
> - **仓管账号**：默认看全部；只有当它 **同时也是负责人**、且传了 `roleName="打包负责人"` 时，才收窄到自己带的大区。
> - 字符串必须 **精确等于 `打包负责人`**，拼错就当没传。与 `/supplier/wms/inventory/team/list` 的 `roleName` 是同一套约定。

列表按申领单 ID 倒序。

#### 响应

`records[]` 单项：

| 字段 | 类型 | 说明 |
|----|----|----|
| `id` | Long | 申领单 ID |
| `applyNo` | String | 申领单号 |
| `teamId` | Integer | 申领大区 ID |
| `teamName` | String | 申领大区名称 |
| `skuId` | Long | 申领物资 SKU ID |
| `skuName` | String | 物资名称（即「物资类别」） |
| `specification` | String | 物资规格 |
| `unitName` | String | 计量单位 |
| `applicantName` | String | 申领负责人姓名 |
| `applyQuantity` | Integer | 申领数量 |
| `issuedQuantity` | Integer | 累计出库数量，未出库为 0 |
| `remainingQuantity` | Integer | 未出库数量 = 申领数量 − 累计出库数量 |
| `progressPercent` | Number | 出库进度百分比，保留两位小数，如 `40.00` |
| `status` | Integer | 申领状态，见「申领单状态」 |
| `statusDesc` | String | 状态中文，直接展示 |
| `rejectReason` | String | 拒绝原因，未拒绝或仓管没填时为空字符串 |
| `createTime` | DateTime | 申领时间 |
| `auditTime` | DateTime | 拒绝时间或历史审批时间，可能为 null |
| `completeType` | Integer | 完成方式：0 未完成、1 自动完成、2 手动完成、3 历史完成 |
| `completeTime` | DateTime | 完成时间，未完成为 null |
| `completeOperatorId` | Long | 完成人 ID |
| `completeOperatorName` | String | 完成人名称（自动完成时为最后一次出库人） |
| `canCancel` | Boolean | 是否显示「撤销」：待审核、未出库、本人提交 |
| `canAudit` | Boolean | 是否显示「拒绝」：待审核、未出库、当前账号是仓管 |
| `canOutbound` | Boolean | 是否显示「出库」：待审核或部分申领、仍有剩余、当前账号是仓管 |
| `canFinish` | Boolean | 是否显示「手动完成」：部分申领、当前账号是仓管 |

#### 响应示例

```json
{
  "id": 88,
  "applyNo": "SL20261104001",
  "teamId": 3,
  "teamName": "昆明一区",
  "skuId": 1012,
  "skuName": "打包纸箱",
  "specification": "60×40×40",
  "unitName": "个",
  "applicantName": "李四",
  "applyQuantity": 50,
  "issuedQuantity": 20,
  "remainingQuantity": 30,
  "progressPercent": 40.00,
  "status": 50,
  "statusDesc": "部分申领",
  "rejectReason": "",
  "createTime": "2026-11-04 09:12:33",
  "auditTime": null,
  "completeType": 0,
  "completeTime": null,
  "completeOperatorId": 0,
  "completeOperatorName": "",
  "canCancel": false,
  "canAudit": false,
  "canOutbound": true,
  "canFinish": true
}
```

> [!TIP]
> **按钮显隐直接用 can\* 字段**
>
> 这四个布尔是后端按「状态 + 出库进度 + 当前账号」算好的，不要在前端自己推。同一条单据对不同人返回的值不同，这是预期的。

### POST /supplier/wms/apply/detail · 申领详情

调用方：仓管 / 本大区负责人

入参 `{ "id": 申领单ID }`。返回列表项的全部字段，另外多出：

| 字段 | 类型 | 说明 |
|----|----|----|
| `remark` | String | 申领备注，无备注时为空字符串 |
| `voucherInfo` | String[] | **这里是真数组**，可直接喂给图片预览组件（提交时是字符串，返回时是数组） |
| `auditorName` | String | 拒绝人或历史审批人姓名，没有时为空字符串 |
| `outboundRecords` | Array | 出库记录，按出库时间、明细 ID 升序，字段见「POST /supplier/wms/apply/item/page」 |
| `cancelTime` | DateTime | 撤销时间，未撤销为 null |
| `cancelOperatorId` | Long | 撤销操作人 ID |
| `cancelOperatorName` | String | 撤销操作人名称 |

非仓管账号查非自己大区的单，返回 `申领记录不存在`（不是无权限），前端按「记录不存在」处理即可。

### POST /supplier/wms/apply/item/page · 出库记录分页

调用方：仓管 / 本大区负责人

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 申领单 ID |
| `index` | Integer | 否 | 页码，默认 1 |
| `size` | Integer | 否 | 页大小，默认 10 |

#### 响应

`records[]` 单项，按出库时间、明细 ID 升序：

| 字段 | 类型 | 说明 |
|----|----|----|
| `id` | Long | 出库明细 ID |
| `applyId` | Long | 所属申领单 ID |
| `quantity` | Integer | 本次出库数量 |
| `issuedQuantity` | Integer | 本次出库后的累计出库数量 |
| `entryId` | Long | 本次生成的大区入库批次 ID |
| `traceId` | Long | 本次生成的库房出库流水 ID |
| `operatorId` | Long | 出库操作人 ID |
| `operatorName` | String | 出库操作人名称 |
| `createTime` | DateTime | 出库时间 |

可见范围同详情：非仓管查非自己大区的单返回 `申领记录不存在`。

## 仓管操作

### POST /supplier/wms/apply/outbound · 申领出库

调用方：仅仓管

对待审核或部分申领的单出库一次，可多次分批出库。每次出库从库房划拨到申领大区（生成一条大区入库批次和一条库房出库流水），并按累计数量自动流转状态：未领满 → 50 部分申领，领满 → 60 已完成（`completeType=1`）。

#### 请求参数

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 申领单 ID |
| `quantity` | Integer | 是 | 本次出库数量，≥ 1，且 ≤ 剩余可出库数量（`remainingQuantity`）、≤ 当前库房剩余可用 |

#### 请求示例

```json
{ "id": 88, "quantity": 20 }
```

#### 响应

| 字段 | 类型 | 说明 |
|----|----|----|
| `itemId` | Long | 本次新增的出库明细 ID |
| `quantity` | Integer | 本次出库数量 |
| `issuedQuantity` | Integer | 本次出库后的累计出库数量 |
| `remainingQuantity` | Integer | 未出库数量 |
| `progressPercent` | Number | 出库进度百分比，保留两位小数 |
| `status` | Integer | 出库后的申领状态（50 或 60） |

#### 响应示例

```json
{ "itemId": 501, "quantity": 20, "issuedQuantity": 20, "remainingQuantity": 30, "progressPercent": 40.00, "status": 50 }
```

> [!WARNING]
> **15 秒防重**
>
> 同一张申领单 15 秒内只能出库一次（`/outbound` 与 `/approve` 共用），重复点击返回 `该申领单过去15秒内进行过出库，请刷新页面，确认未进行重复操作后再次点击`。出库按钮提交后要置灰，成功后刷新详情。

出库弹窗建议把数量默认填成 `remainingQuantity`，上限取 `remainingQuantity` 与 `/apply/sku/stock` 的 `availableStock` 的较小值。

### POST /supplier/wms/apply/approve · 申领出库（兼容入口）

调用方：仅仓管

与「POST /supplier/wms/apply/outbound」逻辑完全相同，只是数量字段名不同，响应也相同。新页面直接用 `/outbound`。

| 字段 | 类型 | 必填 | 说明 |
|----|----|----|----|
| `id` | Long | 是 | 申领单 ID |
| `issuedQuantity` | Integer | 是 | 本次出库数量，规则同 `/outbound` 的 `quantity` |

### POST /supplier/wms/apply/finish · 手动完成

调用方：仅仓管

入参 `{ "id": 申领单ID }`，响应 `data` 为 `null`。

只有 **部分申领**（已出库且未领满）的单可以手动完成，否则返回 `仅部分申领单可以手动完成`。完成后状态 60、`completeType=2`，记录完成时间和完成人；数量和库存都不变。对已完成的单重复调用直接返回成功。

### POST /supplier/wms/apply/reject · 拒绝申领

调用方：仅仓管

入参 `{ "id": 申领单ID, "rejectReason": "库存留给昆明二区" }`。`rejectReason` 后端 **不强制**（≤ 255 字符），产品要求必填请前端自己拦。响应 `data` 为 `null`。

只有 **待审核且未出库**的单能拒绝；已出库（部分申领）的单不能拒绝，只能手动完成。

> [!NOTE]
> **并发下的统一反馈**
>
> 出库、手动完成、拒绝、撤销都带状态条件更新。两个仓管同时操作，后操作的那个会拿到 `申领单状态已变更，请刷新后重试` 或 `申领单状态已变更或不允许出库，请刷新后重试`。**收到这类文案时自动刷新列表并关闭弹窗。**

## 进销存出入库权限与库存口径

申领链路依赖下面三个进销存接口的规则，前端出入库页、库存页要按此渲染。

### POST /supplier/wms/inventory/inbound/create · 新增入库

仅仓库管理员可调用，非仓管返回 `非仓库管理员，无进销存操作权限`。仓管可以选库房或任意大区入库，入口按角色隐藏即可。

### POST /supplier/wms/inventory/outbound/create · 新增出库

| 出库来源 | 谁能操作 | 不满足时的报错 |
|----|----|----|
| `teamId = 0`（库房） | 仅仓库管理员 | `非仓库管理员，无进销存操作权限` |
| `teamId > 0`（大区） | 仓库管理员，或 **该大区的打包负责人** | `无该大区出库权限` |

打包负责人可以登记本大区的物资消耗，但不能从库房直接出货（库房物资通过申领出库发到大区）。出库页的团队选择器按角色渲染：仓管给全量，负责人只给 `/supplier/wms/inventory/team/list` 传 `roleName="打包负责人"` 后返回的大区。

### POST /supplier/wms/inventory/stock/page · 库存页

`teamId = 0` 那一行的 `teamStock` 是「**库房剩余可用**」：

```text
库房剩余可用 = 全局实物库存 − Σ(各大区批次剩余)
```

大区行的 `teamStock` 是该大区剩余。每行都带 `availableStock`，值同库房剩余可用；欠库时两者都允许为负数。全局实物库存字段不在手机端返回。

> [!TIP]
> 发到大区的物资不在库房行重复展示，所以库房行比全局实物小。文案上建议把库房行标成「库房剩余可用」，避免仓管以为库存丢了。

## 搭页面时会用到的进销存接口

申领相关页面不需要新的下拉数据接口，直接复用：

| 用途 | 接口 | 说明 |
|----|----|----|
| 选申领大区 | `/supplier/wms/inventory/team/list` | 传 `{"roleName":"打包负责人"}` 拿到「库房 + 自己带的大区」。**申领页要把 `teamId=0` 的库房项过滤掉**，只留大区。只有 1 个大区时可以隐藏选择器、提交时不传 `teamId` |
| 选物资 | `/supplier/wms/inventory/sku/list` | 传 `{"skuName":"纸箱"}` 或 `{"specification":"60"}` 模糊搜。返回 `skuId / skuName / specification / unitName`。**「物资类别」对应 `skuName`，「物资规格」对应 `specification`** |

### 推荐的提交页交互顺序

1.  `team/list`（带 roleName）→ 过滤掉库房 → 大区下拉；只有一个就选中并隐藏
2.  `sku/list` → 物资类别 / 规格下拉，选中拿到 `skuId`；同一类别多选规格时走 `batchCreate`
3.  `apply/sku/stock`（`id` 传 skuId）→ 展示「库房剩余可用 N 个」，作为数量输入框的 max
4.  上传凭证（可选）→ `JSON.stringify(urls)`，本地先校验张数 ≤ 5、长度 ≤ 512
5.  `apply/create` 或 `apply/batchCreate` → 用返回的 `id` 跳详情或回列表刷新

## 报错文案速查

| msg | 出现在 | 建议处理 |
|----|----|----|
| `无物资申领权限` | 全部申领接口 | 既不是仓管也不是负责人，隐藏整个申领入口；提交接口非负责人也报此文案 |
| `非仓库管理员，无进销存操作权限` | outbound / approve / finish / reject / 库房出入库 | 隐藏仓管操作按钮 |
| `无该大区出库权限` | inventory/outbound/create | 出库页团队下拉按角色收窄 |
| `请选择申领大区` | create / batchCreate | 负责人带多个大区却没传 teamId，前端设为必填 |
| `无权为该大区申领` | create / batchCreate | teamId 不在自己带的大区里，一般是前端串了数据 |
| `物资不存在或已停用` | sku/stock / create / outbound | 刷新物资下拉 |
| `规格[60×40×40]库存不足或数量不合法，最大可申请量: 320` | create / batchCreate | 重新拉 sku/stock 刷新上限 |
| `每批须包含1至100个规格` | batchCreate | 限制多选数量 |
| `重复规格: 1012` | batchCreate | 同一规格只能出现一次 |
| `批量申领包含不存在或停用的规格` | batchCreate | 刷新物资下拉 |
| `批量申领只能选择同一物资类别` | batchCreate | 只允许在同一 `skuName` 下多选规格 |
| `凭证图片最多上传5张` | create / batchCreate | 上传组件限 5 张 |
| `凭证图片URL文本不能超过512个字符` | create / batchCreate | 提交前本地量长度 |
| `凭证图片格式错误` | create / batchCreate | 大概率是传了数组而不是字符串 |
| `申领状态不合法` | page | status 只能传 10/20/30/40/50/60 |
| `开始时间不能晚于结束时间` | page | 校验时间区间 |
| `申领记录不存在` | detail / item/page / 各操作接口 | 越权查看或单据已删，回列表 |
| `仅申请人可以撤销尚未出库的待审核申领单` | cancel | 刷新列表，按 `canCancel` 显隐 |
| `仅部分申领单可以手动完成` | finish | 刷新详情，按 `canFinish` 显隐 |
| `本次出库数量不能超过剩余可出库数量` | outbound / approve | 出库弹窗 max = `remainingQuantity` |
| `库房库存不足，当前可发数量: 12` | outbound / approve | 改小数量，或先入库再出 |
| `该申领单过去15秒内进行过出库，请刷新页面，确认未进行重复操作后再次点击` | outbound / approve | 刷新详情确认出库记录 |
| `申领单状态已变更或不允许出库，请刷新后重试` | outbound / approve | **自动刷新列表并关弹窗** |
| `申领单状态已变更，请刷新后重试` | cancel / finish / reject / outbound | **自动刷新列表并关弹窗** |

## 联调注意

- **测试账号要准备两个**：一个是某大区的 `leader_id`（负责人），一个命中仓管角色。同一个账号两种身份都有时，很多分支测不出来。
- **时间字段**格式与进销存接口一致，`auditTime` / `completeTime` / `cancelTime` 都可能为 null，渲染前判空。
- **voucherInfo 进出不对称**：提交传字符串，详情返回数组。复用同一个表单模型时注意转换。
- **分批出库要看出库记录**：每次出库一条明细，详情的 `outboundRecords` 或 `/apply/item/page` 都能取到；累计数量以 `issuedQuantity` 为准。
- **先看库存页再对申领**：联调时先确认 `stock/page` 库房行的数和你预期的「剩余可用」一致，再测申领链路，否则数字对不上会误判成申领的 bug。
