
中式美食的购物清单不是把几道菜的食材直接拼起来。比如一道菜写“土豆”另一道菜写“马铃薯”还有一道菜写“土豆片”。用户买菜时希望看到的是一个清楚的土豆项但菜谱里原来的叫法也不能完全丢。这个问题如果只按显示名合并清单会重复如果强行改显示名又会显得不自然。问题出在哪里菜谱原始食材数量土豆烧牛肉土豆2个地三鲜马铃薯1个干锅土豆片土豆片300g如果直接按原始名称分组就会有三行。用户真去买菜时还要自己判断这是不是同一种东西。这个体验不对。我把字段拆成两个字段用途ingredientKey给程序合并用displayName给用户看sourceNames保留原始叫法方便追溯sourceRecipeIds保留来源菜谱exportinterfaceShoppingIngredientItem{ingredientKey:stringdisplayName:stringamountText:stringsourceNames:string[]sourceRecipeIds:string[]}ingredientKey 要稳定displayName 要自然。两者不要互相替代。先做小规则不急着做大模型第一版先处理高频别名就够了。规则少一点但要能解释。constaliasMap:Recordstring,string{马铃薯:土豆,土豆块:土豆,土豆片:土豆,小葱:葱,香葱:葱,生抽酱油:生抽}exportfunctionnormalizeIngredientKey(rawName:string):string{constnamerawName.trim().replace(/\s/g,)returnaliasMap[name]??name}这段代码的目的不是覆盖所有食材而是先把购物清单里最常见的重复项压下去。合并时不要丢来源exportfunctionmergeShoppingIngredients(items:RecipeIngredient[]):ShoppingIngredientItem[]{constmapnewMapstring,ShoppingIngredientItem()for(constitemofitems){constkeynormalizeIngredientKey(item.rawName)constexistedmap.get(key)if(!existed){map.set(key,{ingredientKey:key,displayName:key,amountText:item.amountText,sourceNames:[item.rawName],sourceRecipeIds:[item.recipeId]})continue}existed.amountTextmergeAmountText(existed.amountText,item.amountText)existed.sourceNamesArray.from(newSet([...existed.sourceNames,item.rawName]))existed.sourceRecipeIdsArray.from(newSet([...existed.sourceRecipeIds,item.recipeId]))}returnArray.from(map.values())}这里保留 sourceNames 很关键。后面发现合并错了可以直接看到“洋葱”是不是被误合到“葱”里。数量先保守处理数量 A数量 B结果2个1个3个300g200g500g2个300g2个 300g少许1勺少许 1勺functionmergeAmountText(left:string,right:string):string{constaparseAmount(left)constbparseAmount(right)if(aba.unitb.unit){return${a.valueb.value}${a.unit}}return${left}${right}}functionparseAmount(text:string):{value:number;unit:string}|null{constmatchtext.match(/^(\d(?:\.\d)?)(个|g|克|斤|勺)$/)if(!match)returnnullreturn{value:Number(match[1]),unit:match[2]}}单位不一致时不要硬算。买菜清单宁可保守一点也不要把“2个土豆”和“300g土豆片”算成一个看不懂的数字。环境和验证项目说明技术栈HarmonyOS、ArkTS、ArkUI页面餐桌方案页、购物清单页核心文件ShoppingListRepository.ets、IngredientNormalizer.ets、ShoppingListPage.ets验收方式组合多道菜观察购物清单合并结果验收项预期结果土豆/马铃薯/土豆片合并到土豆葱/洋葱不能误合并单位一致可以相加单位不一致保留并列文本functionverifyIngredientKeyMerge(){constresultmergeShoppingIngredients([{recipeId:beef-potato,rawName:土豆,amountText:2个},{recipeId:di-san-xian,rawName:马铃薯,amountText:1个},{recipeId:dry-potato,rawName:土豆片,amountText:300g}])constpotatoresult.find(itemitem.ingredientKey土豆)assert(potato?.sourceRecipeIds.length3)assert(potato?.amountText3个 300g)}总结一下购物清单要解决的是用户真去买菜时的混乱感。ingredientKey 负责把同类食材合到一起displayName 负责让用户看得懂sourceNames 负责后面排错。这个边界立住以后后面加食材替换、库存提醒、推荐菜谱都会更顺。我不直接用 rawName 的原因rawName 最大的问题是它服务的是菜谱表达不是购物清单。菜谱里写“土豆片”很合理因为它描述切法购物清单里写“土豆片 300g”就有点奇怪因为用户买的是土豆不是在菜市场买切好的土豆片。但 displayName 也不能完全抹平。比如“老姜”和“嫩姜”在有些菜里不是一回事“青椒”和“尖椒”也不能随便合并。ingredientKey 的规则要保守宁可先少合并也不要乱合并。食材是否合并原因土豆 / 马铃薯合并常见同义词土豆片 / 土豆块可合并购物清单买的是土豆葱 / 洋葱不合并完全不同食材青椒 / 尖椒先不合并口味和用途可能不同constunsafeAlias{洋葱:葱,尖椒:青椒}// 这类规则先不要写进 aliasMap。// 看起来能减少行数实际会让买菜结果变错。规则怎么慢慢加我不会一次把所有食材别名都写进去。更稳的办法是先从真实购物清单里找重复项哪几组经常一起出现用户又明显会当成同一种东西再加入 aliasMap。观察来源能发现什么多菜谱购物清单高频重复食材用户手动合并记录用户认为哪些是一类搜索词用户常用叫法菜谱来源原始写法差异exportfunctionaddAlias(aliasMap:Recordstring,string,alias:string,target:string){if(aliastarget)returnaliasMapif(!target.trim()||!alias.trim())returnaliasMapreturn{...aliasMap,[alias.trim()]:target.trim()}}页面上怎么解释给用户合并以后页面最好能展开来源。不然用户看到“土豆 3个 300g”会怀疑是不是算错了。exportfunctionbuildSourceText(item:ShoppingIngredientItem):string{returnitem.sourceNames.length1?来自${item.sourceNames.join(、)}:来自${item.sourceNames[0]}}这个小说明很有用。它让合并结果可解释也方便我自己排错。后面如果用户反馈“这个食材不该合并”只要看 sourceNames 就能定位是哪条 alias 规则出了问题。