25. 尺码下拉「同名项重影」:归一 id→codeValue 修复身份错配 —— §24 分页修复揭出的潜伏 bug(2026-06-09)
25.1 现象
编辑某商品(款号 2606080001),尺码已选 M、L。打开尺码下拉滚到底,发现:
- 列表里原本那条 L 没有勾选;
- 末尾额外补了一条 L 并打勾;
- 于是下拉里出现两个 L(一个不勾、一个勾)。M 同理。
25.2 第一反应 vs 真相
第一反应会怀疑「dedup 没写对」或「manualOptions 重复 push」。但真相更微妙:这是 §24 的分页修复(下拉滚动续页)把一个一直潜伏的 bug 揭出来了。
根因是身份错配:这个商品的 sizeIds 落库存的是字典主键 id(8 位),不是 codeValue。于是三处身份不统一:
Select 的 value(回填值) = [id_M, id_L] ← 8 位 id
下拉真实选项 dictData.value = codeValue ← 1~2 位
manualOptions(回填选项).value = 原始存的 id ← 8 位 id
- Select 高亮的是「value 命中 form value」的选项 → 只有 manual 那条(id)被勾,真实选项(codeValue)勾不上。
- dedup 按
value去重,但两条 L 的 value(id vs codeValue)不相等 → 去重失效,两个 L 并存。
为什么 §24 之前看不到? 改分页前下拉只加载 20 条,codeValue 那条 L 在后面的页、根本没加载,列表里只有 manual 那条 → 不重复、还正好勾上,看着「正常」。一旦支持滚动加载全部页,codeValue 那条 L 被拉进来,重影才显形。修复 A(分页)揭出了潜伏 bug B(身份错配)——这是非常典型的「一个改动打开了另一个 bug 的可见窗口」。
25.3 修复:在源头把引用归一成 codeValue
身份系统该统一到哪个?——codeValue。因为下拉选项、保存接口(handleModalOk 回传的就是 i.codeValue)都以 codeValue 为身份,只有「回填 value」这一路混进了 id。所以在加载商品详情时把存的引用统一归一成 codeValue:
services/colorSizeDict.ts新增colorValueMap/sizeValueMap:ref(id|codeValue) → 规范 codeValue。- 主键 id(8 位)→ 该行 codeValue(归一旧数据)
- codeValue → 自身(恒等透传新数据)
- 两套键空间不重叠,同一个 Map 塞两种键不撞车(沿用本文件既有「双键」思路)。
ProductFormModal.tsx:回填 value、manualOptions的 value 全过一遍normalizeRefs/valueMap,统一成 codeValue。字典没加载到(map 空)则回退原值,等价旧行为、不崩。
归一后:form value、manualOptions、dictData 选项全说 codeValue → dedup 生效(两个 L 合一)→ 真实选项命中 form value(正常勾上)。§24 在 useTableSelection 加的 id 兜底匹配,降级为「字典没加载成功时」的防御网,仍保留。
25.4 取舍:为什么在源头归一,不在 FilterDict 里 dedup
❌ 在 FilterDict 按 label 去重:同名不同义有风险,且勾选态仍错(value 没对齐,勾不上)
⚠️ 在 FilterDict 把 id 翻成 codeValue:FilterDict 手里只有已加载的几页 + 手动选项,
字典没全量,翻不可靠
✅ 在加载详情处归一(有全量字典 map):一次翻译,下游 value/选项/保存全部对齐,
dedup 和勾选态都自然正确——身份统一是因,重影/勾不上是果,治因不治果
核心不变量:Select 的 value 必须和它 options 的 value 用同一套身份。一旦两者可能来自不同编码(id / codeValue),就必须在进 Select 之前归一到同一种,否则高亮、去重、回显全部会以隐蔽的方式错乱。
25.5 举一反三
- 「修了 A 冒出 B」≠ 改错了:很多潜伏 bug 靠「另一个限制」遮着(这里是「只加载 20 条」遮住了「身份错配」)。一旦放开限制,潜伏 bug 显形。遇到「我改完这里,那边冒出个怪现象」,先判断是不是揭出了旧坑,而不是急着回滚自己的改动。
- 重复项 / 去重失效,先查「去重键」的身份一致性:dedup 按 value,但 value 来自两个编码空间 → 失效。重复类 bug 的根因常常不在「忘了去重」,而在「去重键不是同一个身份」。
- 同一逻辑字段存了两种编码 = 系统性事实,要在「有完整翻译表的那一层」一次归一,别在下游每个消费点各自 if 特判。
25.6 面试角度
面试话术:「我修下拉无限滚动后,冒出一个『同名选项重影、原项不勾、末尾补一条勾』的怪现象。排查发现不是去重写错,而是分页放开后揭出了潜伏的身份错配——这个商品的尺码引用存的是字典主键 id,而下拉选项和保存接口都以 codeValue 为身份,回填 value 这一路混进了 id,导致按 value 去重失效、按 value 高亮也勾不上。我没有在下游打补丁,而是在加载详情、手握完整字典的那一层,用 id|codeValue → codeValue 的归一表把回填值和回填选项统一成 codeValue,让『value 与 options 同一身份』这个不变量重新成立——去重和勾选态就都自然正确了。这也提醒我:一个改动冒出的新现象,先判断是不是揭出了旧坑。」