feat(consignment): 接单模板反推规格字段,打手端大批量选项改下拉

- 新增 consignment-template-spec-service:按「分类 + 子树」解析接单模板商品名,
  全部可拆成统一维度且无组合碰撞时生成多字段矩阵,否则整类降级为商品名下拉;
  选项只存 ruleKey,取价时实时回查模板,模板停用/删除即无价(missingPricePolicy=block)。
- 匹配引擎:模板驱动配置强制使用 profile specs(不再回退库存旧快照),订单侧按
  product_rule_id 反解规格;解析失败按 unknown 处理,不参与自动匹配。
- 管理端:寄卖配置新增「从接单模板生成」,弹窗给出推荐模式(规格字段 / 商品名下拉)、
  降级原因与组合→模板价覆盖矩阵。
- 打手端:选项超过 8 个改用带搜索的下拉(地铁逃生 100 个商品),规格网格按字段数
  自适应铺满;物品名改用商品名,去掉硬编码「苏尔南」后缀。
- 迁移重编号:合并 main 后寄卖线顺延为 147–151,migrate.ts 保留 legacy→new 幂等映射。
- 同工作区的拼单份额报酬口径、超时重匹配兜底、整单/拼单匹配弹窗调整一并提交。

验证:前端 254/254、后端寄卖相关 41/41、双端 tsc 通过;开发库已实跑
发布→审核入库→自动匹配(模板价 1200 → 匹配报酬 1200)。
This commit is contained in:
2026-11-07 12:26:10 +08:00
parent 50ee3553c5
commit 787b8e2771
38 changed files with 2242 additions and 197 deletions
@@ -36,7 +36,11 @@ export default function ConsignmentWorkOrderMatchModal({
})
const data = candidatesQuery.data?.data
const options = data?.options || []
const needed = Math.max(1, Number(data?.totalQuantity || 1) - Number(data?.activeQuantity || 0))
const totalQuantity = Math.max(1, Number(data?.totalQuantity || 1))
// 整单匹配只需一条库存(由该库存一次占满订单总数量);拼单才按剩余份数选多条。
const needed = data?.sharingEnabled
? Math.max(1, totalQuantity - Number(data?.activeQuantity || 0))
: 1
useEffect(() => {
if (!open) {
@@ -46,9 +50,15 @@ export default function ConsignmentWorkOrderMatchModal({
}
if (options.length === 0) return
// 默认只选精确匹配;非精确库存必须人工勾选并填写原因。
const exactOptions = options.filter((item) => (item.matchType || 'exact') === 'exact')
// 整单还要保证一条库存能一次满足订单总数量,否则默认选中也会被后端拒绝。
const exactOptions = options.filter(
(item) =>
(item.matchType || 'exact') === 'exact' &&
(!data?.sharingEnabled || item.remainingQuantity >= 1) &&
(data?.sharingEnabled || item.remainingQuantity >= totalQuantity),
)
setSelectedIds(exactOptions.slice(0, needed).map((item) => item.inventoryId))
}, [open, options, needed])
}, [open, options, needed, data?.sharingEnabled, totalQuantity])
const nonExactSelected = useMemo(
() =>
@@ -120,8 +130,12 @@ export default function ConsignmentWorkOrderMatchModal({
<Alert
type="info"
showIcon
message={`该工单${data?.sharingEnabled ? `拼单 ${data.totalQuantity} 份,已匹配 ${data.activeQuantity} 份` : '为整单'},本次还需 ${needed} 条库存。`}
description="自动匹配只做精确规格;手动匹配可选多条,拼单工单每选一条生成一个份额。选择非精确库存必须填写原因。"
message={
data?.sharingEnabled
? `该工单拼单 ${totalQuantity} 份,已匹配 ${data.activeQuantity} 份,本次还需 ${needed} 份库存。`
: `该工单为整单匹配,请选择 1 条可一次满足 ${totalQuantity} 件的库存。`
}
description="自动匹配只做精确规格;拼单工单可多选,每条库存按剩余数量生成份额。选择非精确库存必须填写原因。"
/>
<PercentTable<ConsignmentMatchCandidate>
rowKey="inventoryId"
@@ -129,6 +143,8 @@ export default function ConsignmentWorkOrderMatchModal({
pagination={false}
dataSource={options}
rowSelection={{
// 整单只能选一条;拼单可多选,每条库存按剩余数量生成份额。
type: data?.sharingEnabled ? 'checkbox' : 'radio',
selectedRowKeys: selectedIds,
onChange: (keys) => setSelectedIds(keys.map((key) => Number(key))),
}}