前端对接仓库配货 / 打包
打包负责人物资申领
申领提交/撤销/审核 7 个新接口,含凭证字符串格式、canCancel/canAudit 显隐规则,以及出库权限按团队收敛、库房口径改剩余可用两处破坏性变更
Markdown 原文(给 AI 用):https://doc.xfun.fun/wms-material-apply-api.md
梦花仓管小程序 · 进销存
后端模块 xm-bulky-mall-gallery(与现有 /supplier/wms/inventory/* 同一服务)· 测试分支 test-gallery · 2026-08-25
通用约定
跟现有 /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 的中文,速查见 08 节。
⚠️ 唯一的入参怪癖
/apply/sku/stock、/apply/detail、/apply/cancel 三个接口复用了通用的 IdQo,入参字段名都叫 id。其中 /apply/sku/stock 的 id 传的是 skuId,不是申领单 ID,另外两个才是申领单 ID。
角色与状态机
后端从两处判定当前账号的身份,不需要前端传角色来鉴权:
| 角色 | 判定依据 | 能做什么 |
|---|---|---|
| 仓库管理员 | 账号命中配置的仓管角色 | 看全部申领单、同意、拒绝。不能提交申领(除非同时也是负责人) |
| 打包负责人 | xm_bulky_team.leader_id = 当前账号,且团队为启用状态 | 提交申领、撤销自己提交的单、只看自己带的大区的单 |
| 两者都不是 | — | 调任意申领接口都会收到 无物资申领权限 |
一个账号可以同时是仓管和负责人,这种情况下列表默认看全部;如果前端提供了「角色切换」,把当前视角作为 roleName 传给 /apply/page 即可(见 04 节)。
申领单状态
| status | 名称 | 说明 |
|---|---|---|
| 10 | 待审核 | 负责人提交后的初始态。此时库存不动,不占用、不冻结 |
| 20 | 已同意 | 仓管同意,库存此刻才从库房划到大区,issuedQuantity 有值 |
| 30 | 已拒绝 | 仓管拒绝,rejectReason 可能为空字符串 |
| 40 | 已撤销 | 申领人本人撤回,仅待审核态可撤 |
待审核不占库存
提交时只校验一次「申领数量 ≤ 库房剩余可用」,之后不做任何预占。所以两张待审核的单可能都显示能发,实际审批时第二张会因为库存不够被拒。审批失败的 toast 要能正常展示,不要假设提交通过就一定能审批通过。
新增接口 · 负责人侧
POST/supplier/wms/apply/sku/stock查询物资库房剩余可用库存
新增 · 调用方:负责人 / 仓管
选好物资后调用,用于在申领数量输入框旁边展示「当前可申领上限」。
| 请求字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | Long | 必填 | 物资 SKU ID(不是申领单 ID),取自 /supplier/wms/inventory/sku/list 的 skuId |
// 响应 data
{
"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 ~ 10000,且不能超过 availableStock |
voucherUrls | String | 必填 | JSON 数组的字符串,不是数组对象。1 ~ 5 个元素,整串 ≤ 512 字符 |
remark | String | 可选 | 备注,≤ 255 字符 |
🔴 voucherUrls 是字符串,不是数组
后端字段类型是 String,前端要自己 JSON.stringify 后再放进 body:
// ✅ 正确
{ "skuId": 1012, "quantity": 50, "voucherUrls": "[\"https://oss/a.jpg\",\"https://oss/b.jpg\"]" }
// ❌ 会报「凭证图片格式错误」
{ "skuId": 1012, "quantity": 50, "voucherUrls": ["https://oss/a.jpg"] }512 字符是整串长度,包含引号和逗号。OSS 地址长的话 5 张可能放不下 —— 建议前端上传后存相对路径或短地址,并在提交前自己先量一次
voucherUrls.length,超了就地提示,别等后端报错。
响应 data 为 null,成功即可返回列表页刷新。接口不返回新建的申领单 ID,需要跳详情的话得先刷列表拿。
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,不传查全部。传其他值报「申领状态不合法」 |
roleName | String | 可选 | 当前视角角色名。传 "打包负责人" 时强制只看自己带的大区 |
roleName 的作用范围
- 非仓管账号:无论传不传
roleName,永远只能看到自己带的大区的单。 - 仓管账号:默认看全部;只有当它 同时也是负责人、且传了
roleName="打包负责人"时,才收窄到自己带的大区。 - 字符串必须 精确等于
打包负责人,拼错就当没传。这和现有/supplier/wms/inventory/team/list的roleName是同一套约定,小程序当前传的"仓库管理员"走的就是默认(不收窄)分支,角色切换器的取值直接复用即可。
响应 records 单项:
{
"id": 88,
"applyNo": "SL20260825001", // SL + yyyyMMdd + 3位序列
"teamName": "昆明一区",
"skuName": "打包纸箱", // 即「物资类别」
"specification": "60×40×40",
"unitName": "个",
"applyQuantity": 50,
"issuedQuantity": 45, // 仅「已同意」有值,其余状态恒为 null
"status": 20,
"statusDesc": "已同意", // 后端已给中文,直接展示
"createTime": "2026-08-25 09:12:33",
"auditTime": "2026-08-25 10:04:51",
"canCancel": false, // 撤销按钮显隐
"canAudit": false // 同意/拒绝按钮显隐
}✅ 按钮显隐直接用 canCancel / canAudit
这两个布尔是后端按「状态 + 当前账号」算好的,不要在前端用 status===10 && applicantId===me 自己推,列表里也没返回 applicantId。同一条单据对不同人返回的值不同,这是预期的。
POST/supplier/wms/apply/detail申领详情
新增 · 调用方:仓管 / 本大区负责人
入参 { "id": 申领单ID }。返回值是列表项的 超集,在上面所有字段之外多出:
| 字段 | 类型 | 说明 |
|---|---|---|
remark | String | 申领备注,无备注时是空字符串 |
voucherInfo | String[] | 这里是真数组,可直接喂给图片预览组件(提交时是字符串,返回时是数组,注意不对称) |
applicantName | String | 申领人姓名快照 |
auditorName | String | 审核人姓名,未审核时是空字符串 |
rejectReason | String | 拒绝原因,未拒绝或仓管没填时是空字符串 |
cancelTime | DateTime | 撤销时间,未撤销时为 null |
非仓管账号查非自己大区的单,返回 申领记录不存在(不是无权限),前端按「记录不存在」处理即可。
新增接口 · 仓管审核
POST/supplier/wms/apply/approve同意申领
新增 · 调用方:仅仓管
| 请求字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | Long | 必填 | 申领单 ID |
issuedQuantity | Integer | 必填 | 实际出库数量,可以少发但不能多发:必须 ≥1、≤ applyQuantity、≤ 当前库房剩余可用 |
审批弹窗建议把 issuedQuantity 默认填成 applyQuantity,允许仓管改小。响应 data 为 null。
POST/supplier/wms/apply/reject拒绝申领
新增 · 调用方:仅仓管
入参 { "id": 申领单ID, "rejectReason": "库存留给昆明二区" }。rejectReason 后端 不强制(≤255 字符),如果产品要求必填请前端自己拦。响应 data 为 null。
并发下的统一反馈
同意/拒绝/撤销都是 CAS 更新,只有单据仍是「待审核」才会成功。两个仓管同时点,后点的那个会拿到 申领单状态已变更,请刷新后重试。收到这条文案时请自动刷新列表并关闭弹窗,否则用户会一直点一直报错。
既有接口的行为变化 · 必须跟进
这三处不是新接口,是本次一并收口的老接口。前端不跟会出报错或数字对不上。
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,从「全局实物库存」改成了「库房剩余可用」:
库房剩余可用 = 全局实物库存 − Σ(各大区批次剩余)大区行的 teamStock 语义不变(该大区剩余)。响应额外带一个 availableStock 字段,值同库房剩余可用,各行都有。
数字会变小,这是预期的
只要有物资发到过大区,库房那一行的数就会比改版前小。这正是需求要的「发放给大区的部分不再在库房重复展示」。文案上建议把库房行标成「库房剩余可用」,避免仓管以为库存丢了。
注意 physicalStock(全局实物)字段在手机端接口被 @JsonIgnore 掉了,响应里没有,前端拿不到也不需要拿。
搭页面时会用到的既有接口
申领相关页面不需要新的下拉数据接口,直接复用现成的两个:
| 用途 | 接口 | 说明 |
|---|---|---|
| 选申领大区 | /supplier/wms/inventory/team/list | 传 {"roleName":"打包负责人"} 拿到「库房 + 自己带的大区」。申领页要把 teamId=0 的库房项过滤掉,只留大区。返回只有 1 个大区时可以隐藏选择器、create 不传 teamId |
| 选物资 | /supplier/wms/inventory/sku/list | 传 {"skuName":"纸箱"} 或 {"specification":"60"} 模糊搜。返回 skuId / skuName / specification / unitName。「物资类别」对应 skuName,「物资规格」对应 specification |
推荐的提交页交互顺序
team/list(带 roleName)→ 过滤掉库房 → 大区下拉;只有一个就选中并隐藏sku/list→ 物资类别 / 规格下拉,选中拿到skuIdapply/sku/stock(id传 skuId)→ 展示「库房剩余可用 N 个」,作为数量输入框的 max- 上传凭证 →
JSON.stringify(urls),本地先校验张数 ≤5、长度 ≤512 apply/create→ 成功后回列表页刷新
报错文案速查
所有失败响应的 msg 都是可以直接 toast 的中文,无需前端翻译。下表用于确认交互分支:
| msg | 出现在 | 建议处理 |
|---|---|---|
无物资申领权限 | 全部申领接口 | 既不是仓管也不是负责人,隐藏整个申领入口 |
非仓库管理员,无进销存操作权限 | approve / reject / 库房出入库 | 隐藏审核按钮 |
无该大区出库权限 | outbound/create | 出库页团队下拉按角色收窄 |
请选择申领大区 | create | 负责人带多个大区却没传 teamId,前端应设为必填 |
无权为该大区申领 | create | teamId 不在自己带的大区里,一般是前端串了数据 |
物资不存在或已停用 | sku/stock、create | 刷新物资下拉 |
申领数量超过库房剩余可用库存 | create | 重新拉 sku/stock 刷新上限 |
申领数量不能大于10000 | create | 前端 max 设 10000 |
请至少上传1张凭证 | create | 凭证必填 |
凭证图片最多上传5张 | create | 上传组件限 5 张 |
凭证图片URL文本不能超过512个字符 | create | 见 03 节说明,提交前本地量长度 |
凭证图片格式错误 | create | 大概率是传了数组而不是字符串 |
申领状态不合法 | page | status 只能传 10/20/30/40 |
申领记录不存在 | detail | 越权查看或单据已删,回列表 |
申领单状态已变更,请刷新后重试 | cancel / approve / reject | 自动刷新列表并关弹窗 |
实际出库数量不能超过申领数量 | approve | 审批弹窗 max = applyQuantity |
实际出库数量超过库房剩余可用库存 | approve | 刷新后重审 |
联调注意
- 测试账号要准备两个:一个是某大区的
leader_id(负责人),一个命中仓管角色。同一个账号两种身份都有时,很多分支测不出来。 - 时间字段格式与现有进销存接口一致,
createTime/auditTime/cancelTime都可能为 null,渲染前判空。 - 提交接口不回 ID:
create的data是null,需要「提交成功后跳详情」的话,得先刷列表取第一条。 - voucherInfo 进出不对称:提交传字符串,详情返回数组。复用同一个表单模型时注意转换。
- 先看库存页再对申领:库房口径变了,联调时先确认
stock/page库房行的数和你预期的「剩余可用」一致,再测申领链路,否则数字对不上会误判成申领的 bug。