微信小程序里怎么做“我能做 / 差一味可做”的酒库匹配 我做 Naniya 的时候最开始也把重点放在配方库上。经典鸡尾酒整理进去做列表、详情、搜索、收藏再加一点图片看起来就是一个完整的小程序。但真到家庭调酒场景里用户的问题不是“某杯酒标准配方是什么”而是更具体我家里这些材料今晚到底能做什么这件事如果直接丢给大模型答案会很容易变成“看起来合理但不一定能落地”。它可能推荐一杯名字正确的酒也可能悄悄引入用户家里没有的材料。对一个工具型小程序来说这种不确定性太高。所以 Naniya 里的“我能做 / 差一味可做”没有先走生成模型而是先做确定性匹配。第一步库存状态要先稳定酒库里的每个原料至少有三种状态没有。有库存。常备 / 不限量。代码里把可用状态收口成一个函数functionisStocked(status){returnstatusSTOCK_STATUS.IN_STOCK||statusSTOCK_STATUS.UNLIMITED;}这个函数很小但它很关键。后面不管是首页推荐、酒库助手、酒单筛选还是自由调结构都不应该各自理解一遍“什么叫有库存”。第二步原料名不能只靠字符串相等调酒原料很容易出现别名。比如君度、Cointreau、橙皮利口酒在家庭用户眼里可能是同一类东西威士忌、波本、苏格兰威士忌又有上下位关系柠檬、柠檬汁、青柠、青柠汁也经常在配方和实际库存之间混用。所以 Naniya 里维护了原料列表、别名和归一化查找。constSEARCH_NAME_ALIASES{cointreau:[君度,Cointreau,柠莱酒,橙皮利口酒],triple_sec:[橙皮利口酒,Triple Sec,白橙皮酒,橙酒],champagne:[香槟,起泡酒,起泡白葡萄酒],scotch:[威士忌,苏格兰威士忌,三得利角瓶,山崎,白州],};functionlookupIngredientIdByName(name){consttextString(name||).trim();if(!text)return;returnNAME_TO_ID[text]||NORMALIZED_NAME_TO_ID[normalizeIngredientLookupText(text)]||;}最终配方里的原料会尽量被解析成稳定 IDfunctionresolveIngredientId(ingredient){if(!ingredient)return;returningredient.ingredientId||ingredient.iconKey||ingredient.id||lookupIngredientIdByName(ingredient.name)||;}这一步的价值是把“用户怎么说”和“系统怎么匹配”分开。展示可以生活化计算必须稳定。第三步“我能做”就是全量必要原料命中当库存和原料 ID 稳定以后“我能做哪些经典酒”这件事就不复杂了functiongetAvailableDrinks(drinks,inventoryItems){returndrinks.filter(drink{returndrink.ingredients.every(ing{returnisIngredientAvailable(ing,inventoryItems);});});}它的逻辑很明确一杯酒的所有必要原料都在当前库存里命中就算可做。这比“推荐你可能喜欢的酒”更朴素但对家庭调酒更有用。用户不需要先知道酒名也不用点进每个详情页检查缺什么。第四步“差一味可做”不是缺口提示而是增量提示“差一味可做”看起来像一个小功能但实际很影响新手采购决策。新手买调酒材料最容易乱买今天为了一个配方买一瓶明天为了另一个配方再买一瓶。最后家里有很多低频材料却不知道哪个最值得补。Naniya 的逻辑是如果某杯酒不在“可做”里但它只缺一个系统原料就把这个缺失原料聚合起来看补它能多解锁多少杯。核心逻辑在酒库页里constavailableSetnewSet(available);constrecMap{};allDrinks.forEach(drink{if(availableSet.has(drink))return;constmissingSysIdsnewSet();lethasOtherMissingfalse;drink.ingredients.forEach(ing{constidresolveIngredientId(ing);if(id){if(!sysStock.has(id))missingSysIds.add(id);}else{hasOtherMissingtrue;}});if(hasOtherMissing||missingSysIds.size!1)return;constmissingId[...missingSysIds][0];recMap[missingId].delta;});最后再按能解锁的酒款数量排序constrecommendationsObject.values(recMap).sort((a,b)b.delta-a.delta).slice(0,15);consttopRecTextrecommendations.length0?添加「${recommendations[0].name}」可多做${recommendations[0].delta}款:;这不是简单告诉用户“你缺少某某材料”而是告诉他“如果补这个材料能多打开多少选择”。第五步自由调解决经典配方之外的情况还有一种情况用户做不了任何经典酒但材料并不是没用。比如有金酒、苏打水、柠檬、蜂蜜。它可能不完全命中某个标准酒谱但完全可以做一杯家庭长饮。所以 Naniya 里还有自由调结构。它会把库存材料按角色拆分base基酒骨架。lengthener茶、果汁、气泡水等拉长材料。acid柠檬、青柠、西柚、蔓越莓等酸度来源。sweetener糖浆、蜂蜜、甜味利口酒。bitter/modifier/aroma苦味、修饰和香气材料。然后按结构生成可执行方向比如长饮结构functionbuildHighball(base,lengthener,items){if(!base||!lengthener)returnnull;constingredients[ingredient(base.name,45ml,骨架),ingredient(lengthener.name,90-120ml,拉长),];return{structureKey:highball,structureName:长饮结构,methodLabel:直接入杯,ingredients,};}自由调的定位不是替代经典酒谱而是避免系统在“经典配方不足”时直接无话可说。这套设计的边界这不是一个很复杂的推荐系统。它没有做深度个性化也没有把所有判断都交给大模型。它更像一个先把基础事实做稳的家庭工具用户有什么。哪些经典酒能直接做。哪些酒只差一个材料。如果不追求经典还能按什么结构自由调。我觉得这类产品的工程重点不是炫技而是把用户最关心的问题回答清楚。对 Naniya 来说这个问题就是我家里这些材料今晚到底能做什么把这个问题回答清楚以后灵感调酒师、原创酒款、收藏、分享这些功能才有意义。否则页面再多也只是一个资料站。理性饮酒未成年人请勿饮酒。