文件
hl-api-changelog/changelogs-v2/2026-09/03_6986_团期物资从备品库选择-新增接口-管理后台.md
Mimingguang 2f6a989fcf
changelog-filename-gate / validate (push) Failing after 2s
chore(changelog): 补齐 17 条消费闭环 frontmatter 回写
11 条有业务交付改判 verified(#6397/6903/6904/6905/6950/6979/6986/7013/7029/7036/7066,owner=mmg+对应业务 commit ref+交付日 verified_at);
6 条实证零改动改判 not_required(#6014/6016/6140/6938/6842/7087,仅翻 frontend_status 不填 owner/ref)。
#5935 挂起待后端补字段,保持 pending 不动。sync-log 均已记账。
2026-09-06 10:43:20 +08:00

14 KiB

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 6986 团期物资支持从备品库选择配置 admin jw(GIT) 新增接口 deployed verified verified mmg 4d82bdb5 2026-09-03 PR #6987 已合入 dev-v3;2026-09-03 经测试环境网关实测,10 条正负向用例全部通过。前端尚未接入 2026-09-03 dev-v3

团期物资: 新增备品库候选列表接口,并放宽 suppliesName 必填约束

服务: hl-order-service-v3 + hl-resource-service(跨两个服务) PR: #6987 Issue: #6986 影响范围: 管理后台「团期详情 → 物资清单 → 从备品库选择」


⚠️ 关键变化

新增物资行 POST /v3/admin/order/group-batch/:groupBatchId/supplies 的 suppliesName 不再无条件必填。

  • 前端以前以为:suppliesName 是 @NotBlank,任何情况下都得填。
  • 实际现在是:改为 @AssertTrue 二选一——传了 suppliesResourceId 就可以不填名称 (名称、分类、计费方式从备品库取,入参传了也会被库值覆盖);不传 suppliesResourceId 时名称仍必填。
  • 这解决的是原先「从备品库选一条加进来,却仍被迫手填名字」与「名称以库为准」自相矛盾的问题。

部署要求:本次跨 order-v3 与 resource-service 两个服务,必须同时部署。 只发 order-v3 会调不到资源域新增的 /internal/supplies/list-available,候选列表直接报 589518。


一、背景

物资清单原先只能手工逐条录入,名称、分类、计费方式、单价全靠人填,既慢又容易与备品库口径不一致。 本次接入资源域备品库:团期管理员从库里勾选,基础信息由库带出。

资源域改动:新增 GET /internal/supplies/list-available(内部接口,不对外)。 order-v3 侧通过 Feign 调用,并带降级工厂。


二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 团期物资候选列表 GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates 新增接口 从备品库拉可选备品,标记本团期已加入项
2 新增团期物资行 POST /v3/admin/order/group-batch/:groupBatchId/supplies 请求体校验放宽 suppliesName 由必填改为与 suppliesResourceId 二选一

三、接口详情

1. 团期物资候选列表 GET /v3/admin/order/group-batch/:groupBatchId/supplies/candidates

VO: SuppliesCandidateRespVO

使用场景

「物资清单 → 从备品库选择」弹窗打开时调用,渲染可选备品列表。 added=true 的项应显示为已勾选,其 batchSuppliesId 即清单中对应行的 ID,可直接用于删除。

入参

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ — 团期主订单 ID
categoryCode Query String ❌ 字典 supplies_category 分类筛选。传不存在的分类返回空数组,不报错
keyword Query String ❌ — 关键字,匹配备品名称或副标题。中文需 URL 编码

出参 Result<List<SuppliesCandidateRespVO>>

字段 类型 说明
suppliesResourceId Long 备品资源 ID。加入清单时回传本值
suppliesName String 备品名称(来自库)
subtitle String 副标题
category String 分类编码
billingType String 计费方式,已归一为产品域词表 PER_PERSON / PER_QUANTITY
hasCost Boolean 是否计费(资源域 isCharged=1 时为 true)
unitPrice BigDecimal 基础价,加入清单时作为 unitPrice 默认值
unit String 计量单位
added Boolean 是否已加入本团期清单,true 时前端应显示为已勾选
batchSuppliesId Long 已加入时对应的清单行 ID;未加入为 null

请求示例

GET /v3/admin/order/group-batch/1/supplies/candidates?categoryCode=personal_gear

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": [
    {
      "suppliesResourceId": "2023438566624866305",
      "suppliesName": "定制遮阳帽",
      "subtitle": "呼籁旅行专属防晒鸭舌帽",
      "category": "personal_gear",
      "billingType": "PER_PERSON",
      "hasCost": true,
      "unitPrice": 15.00,
      "unit": "顶",
      "added": false,
      "batchSuppliesId": null
    }
  ]
}

空数据 / 降级响应

备品库无匹配项(含分类不存在、关键字无命中)时返回空数组,不报错:

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": []
}

资源域不可达时不静默降级为空,而是显式报 589518,避免前端误判为「库里没有备品」。

错误响应

备品库查询失败(资源域未部署或不可达):

{
  "code": 589518,
  "message": "备品库查询失败,请稍后重试",
  "success": false,
  "data": null
}

业务边界

  • 只读接口,不产生任何写入,可安全重复调用
  • 资源域只返回可用备品,已下架的不在候选池
  • billingType 在 order-v3 侧做过归一,前端不必再兼容资源域原始词表
  • added 依据 suppliesResourceId 匹配;纯手填(无 resourceId)的清单行不会影响任何候选项的勾选态
  • 同一备品在清单中重复存在时,batchSuppliesId 取首条

2. 新增团期物资行 POST /v3/admin/order/group-batch/:groupBatchId/supplies

VO: AddSuppliesReqVO

使用场景

两种来源共用本接口:从备品库勾选加入(传 suppliesResourceId),或手工录入临时物资(传 suppliesName)。

入参

字段 位置 类型 必填 约束 说明
groupBatchId Path Long ✅ — 团期主订单 ID
suppliesResourceId Body Long ❌ 与 suppliesName 二选一 备品资源 ID。传了则名称/分类/计费方式从库取
suppliesName Body String ❌ 与 suppliesResourceId 二选一 变更前 @NotBlank 无条件必填;变更后传了 resourceId 时可空,且传了也会被库值覆盖
quantity Body Integer ✅ @NotNull @Min(1) 数量
category Body String ❌ — 分类,传 resourceId 时以库为准
hasCost Body Boolean ❌ — 是否计费
billingType Body String ❌ — 计费方式
unitPrice Body BigDecimal ❌ — 单价,缺省取库基础价
sortOrder Body Integer ❌ — 排序
remark Body String ❌ — 备注

出参 Result<Long>

字段 类型 说明
data Long 新建清单行 ID(batchSuppliesId),可直接用于删除或调整数量

请求示例

{
  "suppliesResourceId": 2023438566624866305,
  "quantity": 2
}

响应示例

{
  "code": 200,
  "message": "成功",
  "success": true,
  "data": "2095411540751519745"
}

空数据 / 降级响应

本接口必然返回行 ID 或错误,无空数据形态。校验失败时 data 为 null 且 success=false。

错误响应

名称与备品库 ID 都没传(本次新增的二选一校验):

{
  "code": 400,
  "message": "备品名称不能为空(未指定备品库 ID 时必填)",
  "success": false,
  "data": null
}

所选备品不存在或已下架:

{
  "code": 589519,
  "message": "所选备品不存在或已下架,请重新选择",
  "success": false,
  "data": null
}

业务边界

  • 二选一校验在 @AssertTrue 阶段完成,失败时数据库无任何变更
  • 传了 suppliesResourceId 时,入参里的 suppliesName / category / billingType 会被库值覆盖,不是「以入参优先」
  • quantity 最小为 1,传 0 或负数返回 400
  • 同一备品可重复加入,接口不做去重
  • 纯手填行(无 suppliesResourceId)不会出现在候选列表的 added 判定中

四、契约约束与正确调用方式

  • 加入备品库物资时,只需传 suppliesResourceId 和 quantity。不要再从候选列表把名称回填进请求体—— 传了也会被库值覆盖,徒增不一致风险。
  • 手工录入临时物资时,suppliesName 仍然必填。二选一不等于两个都可以不传。
  • 勾选态用 added 判断,删除用 batchSuppliesId。候选列表已经把行 ID 回填好,无需再查一次清单。
  • keyword 含中文必须 URL 编码,否则查询条件丢失(实测未编码时请求异常)。
  • 589518 与「空数组」含义不同:前者是资源域挂了,后者是库里确实没有匹配项。前端不应把 589518 展示成「暂无备品」。
  • 589518 / 589519 是业务码,HTTP 状态仍为 200,判断成败要读响应体的 code。

五、数据库行为

仅描述外部可观察行为:

  • 接口 1(GET candidates)只读,不产生任何写入。
  • 接口 2(POST supplies)新增一条团期物资清单行并返回其 ID;传 suppliesResourceId 时, 名称、分类、计费方式、单价默认值取自备品库快照,落库后不再随备品库变动。
  • 二选一校验与数量校验均发生在写入之前,校验失败时数据库无任何变更(已实测复核)。
  • 本次变更不涉及表结构调整,也不对存量物资行做迁移或回填。

六、边界行为

  • 资源域未部署 / 不可达 → 589518,不降级为空数组
  • 备品库无匹配 → 返回 [],不报错
  • categoryCode 传不存在的分类 → 返回 []
  • 备品库 ID 不存在或已下架 → 589519
  • 既无 suppliesName 又无 suppliesResourceId → 400
  • quantity 为 0、负数或缺失 → 400
  • 未登录 → 401(网关拦截)

六.5、枚举 / 数据字典

billingType(已归一为产品域词表)

取值 含义
PER_PERSON 按人计费
PER_QUANTITY 按数量计费

order-v3 侧已对资源域原始词表做过归一,前端直接消费这两个值即可。

category(字典 supplies_category)

分类编码由字典维护,如 personal_gear(个人装备)、camping_equipment(露营装备)等。 本接口不校验分类是否存在,传未知值返回空数组。


六.6、错误码

段位 589500-589599,owner hl-order-service-v3(GroupBatchErrorCode)。

码 符号 消息 触发
589518 SUPPLIES_LIBRARY_FETCH_FAILED 备品库查询失败,请稍后重试 本次新增。资源域 list-available 返回失败或空结果对象
589519 SUPPLIES_RESOURCE_NOT_AVAILABLE 所选备品不存在或已下架,请重新选择 本次新增。suppliesResourceId 在库中查不到
400 — 备品名称不能为空(未指定备品库 ID 时必填) 本次新增。二选一校验未通过

七、不影响范围

  • 仅影响:管理后台团期详情的「物资清单」模块
  • 零影响:
    • 已有的物资清单查询、调整数量、删除行、确认物料四个接口的出参
    • 存量团期物资行数据(不迁移,新校验只在下次新增时触发)
    • 资源域备品库自身的管理接口(本次只新增一个内部只读端点)
    • 团期状态机与物料确认闸门

八、测试环境已验证

✅ 已验证。 2026-09-03 于测试环境网关实测,真实鉴权(管理端 admin 账号)。

  • 网关:https://api.test.1814.love:9443,分支 dev-v3
  • 前置确认:order-v3 与 resource-service 两侧均已部署(候选列表能返回资源域真实数据即为证)
# 用例 期望 实测
1 GET candidates 无筛选 200,返回候选 ✅ 200,15 条
2 ?categoryCode=personal_gear 200,按分类收窄 ✅ 200,5 条
3 ?keyword=帽(URL 编码) 200,按名称匹配 ✅ 200,1 条「定制遮阳帽」
4 ?categoryCode 传不存在分类 200 空数组 ✅ 200,0 条
5 GET candidates 无 Authorization 401 ✅ 401 缺少有效的 Authorization 头
6 POST 既无名称也无 resourceId 400 ✅ 400 备品名称不能为空(未指定备品库 ID 时必填)
7 POST quantity=0 400 ✅ 400 数量至少为 1
8 POST 缺 quantity 400 ✅ 400 数量不能为空
9 POST suppliesResourceId 不存在 589519 ✅ 589519 所选备品不存在或已下架,请重新选择
10 POST 只传 resourceId 不传名称 放行,名称取库值 ✅ 200,落库 suppliesName="定制遮阳帽" 与库一致,unitPrice=15.00、billingType=PER_PERSON

行为变更核对:用例 10 是本次核心——入参未传 suppliesName 仍成功建行,名称由备品库带出。

联动核对:加入后重查候选列表,该项 added=true、batchSuppliesId 回填为新建行 ID;删除后回到 added=false。

未落库核对:用例 6–9 执行后复查清单仍为 0 条,证明校验失败不产生写入。

测试数据还原:用例 10 建的行已 DELETE 删除,清单由测试前 0 条恢复为 0 条。

本地单元测试(提交信息记载):GroupBatchSuppliesServiceTest 新增 208 行用例。


十、相关文档

  • 团期需求文档:docs/group/(dev-v3 分支)
  • 实施单 AC-TD-13(ad747212d 已标记 #6986 落地并勘误)

关联 / 联系人

  • Issue: #6986
  • PR: #6987(合并提交 bcf9332bd,落入 dev-v3)
  • 后续修正: 8806315e3(备品去重,同时关联 #6950)
  • 服务: hl-order-service-v3 + hl-resource-service
  • 后端: jw
  • 前端: 待认领(frontend_status: pending)