微信小程序里怎么做“我能做 / 差一味可做”的酒库匹配
2026/7/8 8:27:27 网站建设 项目流程

我做 Naniya 的时候,最开始也把重点放在配方库上。

经典鸡尾酒整理进去,做列表、详情、搜索、收藏,再加一点图片,看起来就是一个完整的小程序。

但真到家庭调酒场景里,用户的问题不是“某杯酒标准配方是什么”,而是更具体:

我家里这些材料,今晚到底能做什么?

这件事如果直接丢给大模型,答案会很容易变成“看起来合理,但不一定能落地”。它可能推荐一杯名字正确的酒,也可能悄悄引入用户家里没有的材料。对一个工具型小程序来说,这种不确定性太高。

所以 Naniya 里的“我能做 / 差一味可做”没有先走生成模型,而是先做确定性匹配。

第一步:库存状态要先稳定

酒库里的每个原料至少有三种状态:

  • 没有。
  • 有库存。
  • 常备 / 不限量。

代码里把可用状态收口成一个函数:

functionisStocked(status){returnstatus===STOCK_STATUS.IN_STOCK||status===STOCK_STATUS.UNLIMITED;}

这个函数很小,但它很关键。后面不管是首页推荐、酒库助手、酒单筛选,还是自由调结构,都不应该各自理解一遍“什么叫有库存”。

第二步:原料名不能只靠字符串相等

调酒原料很容易出现别名。

比如君度、Cointreau、橙皮利口酒,在家庭用户眼里可能是同一类东西;威士忌、波本、苏格兰威士忌又有上下位关系;柠檬、柠檬汁、青柠、青柠汁也经常在配方和实际库存之间混用。

所以 Naniya 里维护了原料列表、别名和归一化查找。

constSEARCH_NAME_ALIASES={cointreau:['君度','Cointreau','柠莱酒','橙皮利口酒'],triple_sec:['橙皮利口酒','Triple Sec','白橙皮酒','橙酒'],champagne:['香槟','起泡酒','起泡白葡萄酒'],scotch:['威士忌','苏格兰威士忌','三得利角瓶','山崎','白州'],};functionlookupIngredientIdByName(name){consttext=String(name||'').trim();if(!text)return'';returnNAME_TO_ID[text]||NORMALIZED_NAME_TO_ID[normalizeIngredientLookupText(text)]||'';}

最终配方里的原料会尽量被解析成稳定 ID:

functionresolveIngredientId(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 的逻辑是:如果某杯酒不在“可做”里,但它只缺一个系统原料,就把这个缺失原料聚合起来,看补它能多解锁多少杯。

核心逻辑在酒库页里:

constavailableSet=newSet(available);constrecMap={};allDrinks.forEach(drink=>{if(availableSet.has(drink))return;constmissingSysIds=newSet();lethasOtherMissing=false;drink.ingredients.forEach(ing=>{constid=resolveIngredientId(ing);if(id){if(!sysStock.has(id))missingSysIds.add(id);}else{hasOtherMissing=true;}});if(hasOtherMissing||missingSysIds.size!==1)return;constmissingId=[...missingSysIds][0];recMap[missingId].delta++;});

最后再按能解锁的酒款数量排序:

constrecommendations=Object.values(recMap).sort((a,b)=>b.delta-a.delta).slice(0,15);consttopRecText=recommendations.length>0?`添加「${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 来说,这个问题就是:

我家里这些材料,今晚到底能做什么?

把这个问题回答清楚以后,灵感调酒师、原创酒款、收藏、分享这些功能才有意义。否则页面再多,也只是一个资料站。

理性饮酒,未成年人请勿饮酒。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询