用 AI 写代码做家庭调酒小程序:真正难的是把酒库到保存跑通

📅 2026/7/4 19:41:45
用 AI 写代码做家庭调酒小程序:真正难的是把酒库到保存跑通
用 AI 写代码做家庭调酒小程序真正难的是把“酒库 - 可做 - 保存”跑通我一开始以为做一个家庭调酒小程序核心应该是配方库。经典鸡尾酒整理进去做列表、搜索、详情、收藏再配几张好看的图表面上就像一个完整产品。但真把 Naniya 做成微信小程序后我发现家庭调酒里更高频的问题不是“某杯酒的标准配方是什么”而是我家里就这些材料今晚到底能做什么这篇不讲“AI 写代码有多神”而是复盘一个更具体的产品工程问题当一个小程序不是资料站而是要帮用户从真实库存出发完成一次调酒它的数据链路应该怎么设计。入口为什么不是搜索框Naniya 现在的主入口是四个 tab今日、酒单、酒库、我的。这看起来很常规但关键是“酒库”不是附属功能而是主路径的起点。用户先把家里已有的基酒、利口酒、葡萄酒、软饮果汁、糖浆、苦精香料、新鲜食材和装饰调味记进去后面的推荐、筛选、自由调和创作才有依据。源码里这部分不是一个简单的字符串列表。utils/inventory-config.js里维护了原料分类、原料 id、展示名、搜索别名和库存状态constSTOCK_STATUS{NONE:none,IN_STOCK:inStock,UNLIMITED:unlimited,};库存判断也尽量收口到统一函数里functionisStocked(status){returnstatusSTOCK_STATUS.IN_STOCK||statusSTOCK_STATUS.UNLIMITED;}functionisIngredientAvailable(ingredient,inventoryItems,customIngredients){if(!ingredient)returntrue;if(ingredient.customIngredientId){returnisStocked(inventoryItems[custom_ingredient.customIngredientId]);}constidresolveIngredientId(ingredient);if(!id){constnameString(ingredient.name||).trim();return!!name(customIngredients||[]).some(ci{constkeycustom_(ci._id||ci.id||);returnisStocked(inventoryItems[key])ci.namename;});}returnisStocked(inventoryItems[id]);}这个设计的价值在于后面不管是“首页今日推荐”“酒单筛选”“酒库助手结果页”还是“只用我的材料”的灵感调酒师都可以复用同一套库存事实而不是各写一份判断。库存到经典配方先把确定性链路做稳“我能做哪些经典酒”不需要一上来就用大模型。它更适合做成确定性判断一杯酒的所有必要原料都能在库存中命中就算可做。源码里的核心逻辑很直接functiongetAvailableDrinks(drinks,inventoryItems){returndrinks.filter(drink{returndrink.ingredients.every(ing{returnisIngredientAvailable(ing,inventoryItems);});});}难点不在这几行而在“原料是否命中”这件事不能太脆。比如同一个东西可能有中文名、英文名、别名、品牌名也可能来自用户自定义原料。Naniya 里会用resolveIngredientId、NAME_TO_ID、SEARCH_NAME_ALIASES和自定义原料的 parent 归因去尽量把它归一到稳定 id。酒单页也不是只做搜索。它有两个和酒库强绑定的过滤“我能做”缺失原料数为 0。“补 1 样可做”缺失原料数为 1。后端getDrinkList会从drink_search_index读取酒款索引必要时 fallback 到drinks集合再按搜索词、风味标签和库存模式过滤。这里的判断仍然是确定性的constmissingCountArray.from(newSet(ids)).filter(id!available.has(id)).length;if(modeavailable)returnmissingCount0;if(modemissing_one)returnmissingCount1;这样做有一个好处用户看到“可做”时背后不是模糊推荐而是能追溯到自己的酒库输入。结果分层经典、自由调、补材料不要混在一起如果把所有答案都塞进一个列表用户还是要自己判断下一步该干什么。所以 Naniya 的酒库结果页分成三类经典库存已经满足的标准酒谱可以直接看详情和做法。自由调不追求复刻经典而是用已有材料组出长饮、酸甜、气泡、短饮等结构。补材料如果只差一种原料就告诉用户补它能多做哪些酒。这里面最容易被误解的是“自由调”。它不是一句“AI 推荐你随便发挥”而是一个结构生成器。utils/structure-mix-builder.js会先把库存里的原料转成带角色的 item例如base基酒骨架。lengthener茶、果汁、气泡等拉长材料。acid柠檬、青柠、西柚、蔓越莓等酸度来源。sweetener糖浆、蜂蜜、甜味利口酒。bitter/modifier/aroma苦味、修饰和香气材料。再按结构组合// 长饮基酒 拉长材料可选酸、甜和香气buildHighball(base,lengthener,items);// 酸甜基酒 酸 甜buildSour(base,acid,sweetener,items);// 气泡开胃风味核心 气泡buildSpritz(modifier,sparkling,items);// 搅拌短饮基酒 副调buildStirred(base,modifier,items);// 家用基底基酒 茶/果汁/咖啡/奶感材料buildTeaOrJuice(base,body,items);这类结果对家庭场景很重要。用户可能没有做一杯标准玛格丽特所需的完整材料但有金酒、苏打水、柠檬、蜂蜜。系统就不应该只回答“经典配方不足”而应该告诉他可以走一个更现实的长饮结构。“补材料”也是同理。源码里不会推荐一堆泛泛的采购清单而是只处理“恰好缺一种系统原料”的酒款把缺失原料聚合起来按能解锁的酒款数量排序if(hasOtherMissing||missingSysIds.size!1)return;constmissingId[...missingSysIds][0];recMap[missingId].delta;recMap[missingId].drinks.push({id:drinkId,name:drink.name||,});这就是为什么“补材料”对新手有用。它不是在种草某个瓶子而是在回答“买这一样东西能让我的酒库多打开哪些可能”。生成能力要被约束而不是只接一个输入框Naniya 里还有一个更偏创作的入口叫“灵感调酒师”。用户可以输入一个场景、情绪或物件比如“雨夜旧书店”“朋友来家里”“饭后清爽”也可以点“只用我的材料”。这类功能很容易写成“把用户输入丢给大模型然后展示一段漂亮文案”。但在调酒场景里这样不够。因为用户真正需要的是一杯可以照着做、可以修改、可以保存的酒而不是一个名字很好听但材料不闭合的创意。所以前端在构造请求时会把创作边界一起传给后端intent:{rawText:startContext.rawText,targetLabel:startContext.rawText,targetTechnique:none,timeConstraint:mode.timeConstraint,materialPolicy:{mode:materialMode},}其中materialPolicy.mode有几个关键取值open_special今晚能喝偏当天可执行。creative_special开脑洞特调允许更开放的现实材料和技法。inventory_strict只用我的材料。当用户选择“只用我的材料”或者输入里出现“根据我的酒库/只用现有材料”这类意图时后端aiBartenderCreate会读取用户库存构造可用材料范围。之后配方校验会检查材料闭包不该在inventory_strict模式里把清单外材料写进配方、步骤或说明。如果生成结果没有通过结构校验后端不会硬把它包装成可操作做法而是返回 fallback{status:fallback,recipe:null,requirementChecks:[{key:structure_validation,label:结构校验,status:failed,text:这版配方没有通过结构校验我不会把它展示为可操作做法。}]}这里的工程判断是AI 适合处理开放灵感但不能替代业务约束。用户看到的最终结果仍然要回到材料、结构、比例、步骤和保存状态。保存闭环比生成本身更重要如果灵感调酒师只是生成一次那它更像玩具。Naniya 里生成结果后可以继续走到“存为作品”前端会把生成结果整理成自定义酒款表单的预填数据constprefill{name:recipe.name||,nameEn:recipe.nameEn||,description:recipe.oneLineTaste||,flavor:recipe.tasteProfile,ingredients:recipe.ingredients.map(item({name:item.name,amount:item.amountText,iconKey:item.ingredientId||,})),steps:recipe.steps||[],};wx.setStorageSync(prefillCustomDrink,prefill);wx.navigateTo({url:/pages/custom-drink/custom-drink?prefill1});自定义酒款页再让用户补齐或修改基本信息酒名、英文名、一句话描述、酒精度、杯型、标签、图片。风味配方甜、酸、苦、烈、果香、草本以及原料用量和制作步骤。预览确认把酒款当成一个结构化作品检查。保存私有保存或提交审核。后端saveCustomDrink也会做基本校验酒名不能为空必须有步骤、风味、图片和原料原料不能只有单位没有数值公开能力关闭时会强制保存为私有。这条链路才是产品闭环酒库输入 - 可做经典 / 自由调 / 补材料 - 查看做法或进入创作 - 生成结构化配方 - 保存为自己的特调 - 之后继续修改、收藏或复做很多功能单独看都不难列表、详情、搜索、AI 输入框、收藏、分享、表单。真正难的是让它们围绕同一个用户问题工作。这次项目里我学到的几件事第一AI 写代码会放大执行速度也会放大产品发散。页面可以很快搭出来云函数也可以很快补上但如果没有明确主路径就会出现一堆“看起来都对”的功能。配方库、智能生成、收藏、分享、公开酒款、个人酒库都合理但它们不天然构成用户路径。第二能确定的逻辑先确定。“这杯酒能不能做”“差几种原料”“补哪个原料能多做几杯”这些问题不应该交给模型猜。Naniya 里这部分尽量使用库存、原料 id、配方索引和缺失数量来计算。第三AI 只应该出现在模糊度高的地方并且要有边界。“雨夜旧书店”这种灵感确实需要生成能力但生成结果必须接受材料、结构、安全和可执行性的约束。否则它只是文案不是产品能力。第四闭环的最后一步往往不是“展示结果”而是“沉淀结果”。用户真正调出一杯喜欢的酒后要能保存、修改、复做。否则一次生成再惊艳也很难变成长期使用的工具。所以我现在更愿意把 Naniya 看成一个“从家庭酒库出发的调酒工作流”而不是一个鸡尾酒配方库。配方库可以回答“某杯酒怎么做”。酒库闭环要回答的是“我现在有什么今晚能喝什么下一步还能怎么做。”理性饮酒未成年人请勿饮酒。