说出来你可能不信,我写 BrewUI 的起因特别朴素:当时手头攒了三十多份精酿配方,一半是聊天记录里的截图,一半是手写笔记本上的潦草数字,还有两份存在已经打不开的旧硬盘里。每逢周末酿酒,光翻配方就要花半小时,更别提每次糖化温度差两度、苦度计算全凭感觉,连同一款配方做三次味道都不太一样。于是我一咬牙,决定给自己写一套顺手的酿造管理工具,把配方、批次、糖化步进、发酵记录全部集中到一块屏上,这就是 BrewUI 的由来。
这篇文章会把 BrewUI 从需求拆解、数据建模、后端设计到前端适配的完整思路和踩坑过程都摊开讲。如果你也是自酿爱好者、家酿进阶玩家,或者正在琢磨给某个垂直场景做一套小而美的工具,应该能从这里找到一些可以直接落地的方案。整套东西不复杂,但每块背后都有取舍,我会把“为什么这么选”也一并说清楚。
1. 项目定位:为什么需要一个属于自己的 BrewUI
1.1 精酿圈的真实痛点:配方散落与批次丢失
自酿圈子里有个特别普遍的现象:很多人是从一包原料包起步的,当时觉得照着说明书来就行,后来配方越玩越多,问题就全暴露了。麦芽多少克、酒花分几次加、酵母菌种编号、发酵温度曲线,这些信息分散在不同渠道,想整理的时候,最原始的载体反而是手机备忘录和截图纸。更麻烦的是,同一款酒在不同季节做,水质和室温都不一样,如果没有记录,下次想复现口感基本靠猜。
我见过不少酿友用 Excel 管理配方,把列名做得漂漂亮亮,能排序能筛选,乍一看够用。但真正做酒的时候,Excel 解决不了流程问题:糖化阶段的升温步进需要按时间提醒,熬煮阶段的酒花添加时间点需要手动计数,发酵阶段每天测比重还得自己找地方登记。工具一旦需要人分心去“管理”,而不是在过程里“顺手记录”,很快就会被放弃。
BrewUI 要解决的,就是把配方、批次、操作记录这三件事连成一条线。配方是静态资产,批次是每次实际酿造的过程实例,操作记录则是连接两者的时间轴。这三点打通之后,一次酿造从头到尾都能留痕,下次复酿直接基于历史批次数据调整,而不是重新发明轮子。
1.2 现成软件的两类短板:要么重要么散
在决定自研之前,我把市面上常见的酿酒软件大致翻了一遍,结论是它们分成两个极端。一类是专业级的桌面端工具,功能极度丰富,支持营养酵母数据库、水化学全项计算、配方成本分析甚至酒标设计,但学习成本高,很多功能我根本用不上,而且这类软件大多没有好的移动端配套,糖化间里一手麦芽一手手机的时候操作很别扭。
另一类是社区里流行的现成 Web 计算器,查个 IBU、算个 ABV 很方便,但它们是“点状”的,算完就完了,没有任何持久化,配方和结果之间无法建立联系。说白了,那些工具解决的是单点计算,而酿造是一个连续过程,我需要的是一个能把过程串起来的系统。
BrewUI 的差异化思路是“轻量但闭环”。它不追求覆盖专业酿造软件的全部功能,但一定保证配方录入、批次创建、步骤记录、数据回看这些核心动作从头到尾是通的。每个环节产生的数据都挂在某个批次上,而每个批次又对应着一张配方,不存在信息孤岛。
1.3 BrewUI 的核心设计原则
动手写代码之前,我给自己定了三条原则,这三条原则在后续开发中帮我挡住了不少折腾。
第一,数据录入要克制。酿造现场最怕的就是表单套表单,一屏下来十几个输入框,手忙脚乱时只会乱填。所以 BrewUI 的每个页面只聚焦一个动作,比如“添加麦芽”“记录糖化温度”“登记比重”,所有字段都控制在一屏以内。
第二,默认值要聪明。不同风格的啤酒,糖化温度、酒花用量、酵母投放量都有自己的典型区间。新表单出现时,根据配方风格自动预设合理默认值,现场操作只需要微调,录入成本大幅降低。这一点听起来简单,实际体验提升非常明显。
第三,历史数据要能被追溯和复用。每一次酿造结束,批次下的所有数据都以时间为序沉淀下来,包括操作日志、传感器读数、手动记录和最终成品评价。这些数据既服务于复酿调优,也方便对比同款配方的不同批次差异。
这套原则落到实现上,本质上就是在说:BrewUI 是一个以配方为骨架、以批次为单位的酿造记录系统。现在很多自酿工具的问题在于它们是“给人看的仓库”,而我想做的是“陪人酿酒的操作台”。
2. 配方模型建模与数据表设计
2.1 核心概念:麦汁、糖化效率、苦度与色度
酿酒配方的核心参数无非四样:原料构成、糖化步进、熬煮计划、发酵预期。要设计数据表,必须先理解这些参数之间的依赖关系。
麦汁浓度(OG)取决于麦芽重量、糖化效率和最终体积。举个例子,目标 OG 是 1.050,批次体积 20 升,如果糖化效率按 70% 算,那需要投放的麦芽量大概在 4.2 公斤上下。这个数字不是拍脑袋定的,背后是“每公斤麦芽在理想情况下能贡献约 300 个比重点,乘以效率之后除以体积”的经典公式。
苦度(IBU)和色度(SRM)则是一组累积计算。酒花的 α 酸含量乘以投放量和利用率,再除以体积,得出理论苦度;不同色度的麦芽按重量加权,得到麦汁的预测色度。这些计算在配方录入的当下就可以实时预览,让酿酒师在保存配方前就看到成品趋势,而不是等酿完才发现颜色太深、苦度太高。
BrewUI 在配方表设计上,把“配方头”“原料明细”和“步骤计划”拆成三张独立表。配方头保存风格、批次体积、目标 OG/FG、煮沸时间等全局参数;原料明细表用外键关联配方 ID,每一行是一条麦芽或酒花记录;步骤计划表存放糖化步进,包括温度、保持时间和是否循环等标志位。这种拆分的好处是后续按风格筛选、按原料反查时不需要解析长文本,直接结构化查询就行。
2.2 数据表划分与字段设计
这里贴一下我在 SQLite 里实际使用过的核心表结构,简洁但够用:
-- 配方主表
CREATE TABLE recipes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
style TEXT DEFAULT 'IPA',
batch_volume REAL DEFAULT 20.0, -- 批次体积(L)
og_target REAL DEFAULT 1.050, -- 目标原麦汁浓度
fg_target REAL DEFAULT 1.010, -- 目标终麦汁浓度
ibu_target REAL DEFAULT 40.0, -- 目标苦度
srm_target REAL DEFAULT 8.0, -- 目标色度
brewhouse_efficiency REAL DEFAULT 70.0, -- 糖化效率(%)
created_at TEXT DEFAULT (datetime('now'))
);
-- 原料明细表
CREATE TABLE recipe_ingredients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipe_id INTEGER NOT NULL,
ingredient_type TEXT NOT NULL, -- malt / hop / yeast / other
name TEXT NOT NULL,
amount REAL NOT NULL, -- 数量(g)
unit TEXT DEFAULT 'g',
add_time INTEGER, -- 添加时间点(分钟,煮沸倒计时)
alpha_acid REAL, -- 酒花α酸(%)
color_lovibond REAL, -- 麦芽色度(Lovibond)
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
);
-- 批次主表
CREATE TABLE batches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipe_id INTEGER NOT NULL,
batch_no TEXT NOT NULL, -- 批次编号
brew_date TEXT NOT NULL,
og REAL,
fg REAL,
volume REAL,
status TEXT DEFAULT 'planning', -- planning / brewing / fermenting / conditioning / done
notes TEXT,
FOREIGN KEY (recipe_id) REFERENCES recipes(id)
);
这套结构的核心想法是让配方和批次保持“一对多”关系。同一张配方可以反复酿造,每次酿造生成一个独立批次,互不污染。原料明细表里的
add_time
字段同时服务于麦芽投放顺序和酒花添加时间,用整数分钟表示煮沸倒计时,这样前端可以直接按时间轴渲染操作步骤,不需要额外解释语义。
还有一个细节容易被忽略,就是单位。原料表里我统一用克做基准单位,投放量、苦度计算、成本统计都基于克,避免磅、盎司、升、加仑混用带来的换算错误。虽然 UI 上允许用不同单位录入,但底层存储永远是标准单位,换算逻辑收敛到一层,出了错也容易排查。
2.3 配方计算逻辑:为什么必须内置标准值库
配方计算器能不能算出靠谱的 IBU 和 SRM,关键在于内置标准值库是否完整。比如马里斯奥特麦芽(Maris Otter)的色度通常在 3-6 Lovibond,卡斯卡特酒花的 α 酸含量则在 4.5%-7% 之间波动。如果每次录入都让用户手填这些值,既容易填错,也不方便跨配方比较。
BrewUI 的做法是内置一份精简的常用麦芽和酒花数据库,覆盖麦芽的色度范围、典型出糖率,以及酒花的 α 酸范围、典型风味描述。新增原料时默认带入典型值,用户可按实际采购批次修正。这样一来,配方计算在录入阶段就有意义,也能在酿酒前精准估算风格匹配度。
IBU 计算我采用的是 Tinseth 公式,这是家酿圈用得最广的算法,公式如下:
利用率 = 1.65 * 0.000125^(比重-1) * (1 - e^(-0.04 * 时间)) / 4.15
IBU = (酒花重量(g) * α酸(%) * 利用率 * 1000) / 体积(L)
这个公式不是唯一的苦度计算方法,Rager 公式在早期煮沸阶段的数值通常会偏高一点,但 Tinseth 的整体曲线更贴合现代精酿风格的感知,社区接受度也最高。BrewUI 默认用它,同时留了接口,后续可以把 Rager 作为可选项加进去。
SRM 色度计算则相对简单,用麦芽重量乘以各自色度,求和后除以总体积,再乘以一个常数修正。这个值不需要百分之百精确,它更多是做配方风格对照用,比如新英格兰 IPA 的 SRM 通常在 4-8 之间,世涛则在 30-40 范围内。只要量级对,酿酒师心里就有数。
我实际测试下来的体感是,内置标准值库的最大价值不是省了那几秒输入时间,而是减少了“因为拼错原料名导致计算偏差”的隐性错误。标准值库配上本地模糊搜索,输入 “cascade” 自动补全成 Cascade,并带出默认 α 酸值,整个录入流程顺畅很多。
3. 后端接口与酿造批次状态机
3.1 批次状态流转:从酝酿到装瓶
配方建模完成之后,下一个核心问题是批次状态怎么管理。一次酿造从开始到能喝,中间跨越好几天甚至几周,流程大致是:计划(planning)→ 糖化(brewing)→ 发酵(fermenting)→ 熟化(conditioning)→ 完成(done)。
如果只把这些状态做成一个普通字符串字段,那和 Excel 没什么区别。BrewUI 里的状态机价值在于:每个状态转换都绑定触发条件和时间记录,状态变成什么、什么时候变的、当时准备了哪些操作,全部有据可查。
我在实现时没有引入完整的状态机框架,而是用一个轻量的状态映射表来约束合法流转路径。比如从
brewing
到
fermenting
必须满足“已录入 OG”和“酵母已投放”两个条件,否则前端会提示缺数据。这个约束从产品层面防止了“酿到一半忘了记数据,最后补录全靠回忆”的情况。
状态流转的细节我还加了一个小设计:状态变化自动生成一条系统日志,记录变化时间、旧状态、新状态和触发动作。日志不可手动编辑,只能追加备注。这样的设计在后期复盘时特别好用,我能清楚看到一批酒在哪个环节卡了多久,哪个批次又是因为升温太慢导致发酵启动迟缓。
3.2 关键接口设计与日志记录
后端我选的是 Flask + SQLite 的组合,原因很简单:单机部署、数据量不大、请求模型简单,没必要上重型数据库。接口设计围绕“资源 + 动作”展开,核心接口如下:
GET /api/recipes -- 配方列表(支持风格过滤)
POST /api/recipes -- 新建配方
GET /api/recipes/{id} -- 配方详情(含原料和步骤)
PUT /api/recipes/{id} -- 更新配方
POST /api/recipes/{id}/duplicate -- 复制配方(想做实验版时很有用)
GET /api/batches -- 批次列表(支持状态过滤)
POST /api/batches -- 基于配方创建批次
POST /api/batches/{id}/status -- 更新批次状态
POST /api/batches/{id}/readings -- 添加过程记录(比重/温度/备注)
GET /api/batches/{id}/timeline -- 获取完整时间线
这里我想重点说下
readings
接口。它接收的不是一个固定结构体,而是一个
kind
枚举加一个 JSON 扩展字段。比重、糖化温度、室温、pH 值都能落到同一张表里,用类型字段区分。这样做的好处是前端不需要为每种读数单独写接口,新增监控项也不用改表结构。
实际记录的时候,每次手动录入读数还会顺带记下当前时间和操作人(如果做了多用户),形成一条带时间戳的过程记录。后期做批次对比时,我把两条批次的时间线并排看,温度曲线的差异一眼就能看出来,比翻笔记高效得多。
3.3 对接温度传感器的思路
作为一个深度自酿党,我一直对发酵温度控制很在意。很多酒的异味都来自发酵前三天温度失控,所以 BrewUI 里预留了传感器数据接入的口子。我用的是一块带 WiFi 的 ESP32 加 DS18B20 防水温度探头,泡在发酵桶的测温井里,每 5 分钟上报一次数据。
后端只增加了一个简单的接收端点,传感器通过 HTTP POST 方式把温度数据推送到 BrewUI,由服务端统一写入
readings
表。这么做的好处是传感器端逻辑极简,坏了换一块重新烧固件就行,不碰后端核心逻辑。
Web 界面上,批次详情页会显示最近 24 小时的温度曲线,前端用 Canvas 画折线图,不引入大体积图表库。对发酵监控来说,曲线的形状比单个数值有意义得多,我每天瞄一眼曲线是否平滑,就能判断发酵是否正常。这块功能做出来后,我明显感觉睡觉踏实了,之前半夜总惦记着温度会不会飙高。
4. 前端界面与移动端适配
4.1 设备与屏幕的取舍
发酵间和糖化间里,手机是第一设备,其次才是平板和电脑。所以 BrewUI 的前端设计从一开始就贯彻 Mobile First,画原型都从 375px 宽的屏幕开始设计,确保大拇指能轻松点到大按钮,而不是设计完桌面版再硬缩到手机上。
真正动手开发时,我没有选择 React Native 或 Flutter 那种重方案,直接走了 PWA 路线。后端返回数据,前端用 Vue 3 加 Vant 组件库,一个响应式 Web 应用,支持安装到手机主屏,离线也能打开已缓存页面。对单人或两三人协作的家酿场景来说,PWA 的跨平台能力和开发效率完全够用。
这里有个实用经验:字体和按钮尺寸一定要够大。在酿造现场,手经常湿漉漉的,屏幕上有水或者手上沾着麦芽汁,点击精度会大幅下降。Vant 的按钮默认高度是 44px,我全站统一改成了 52px,误触率肉眼可见地降了下来。
4.2 表格优先,树形次之:数据展示的核心策略
酿造数据的组织方式天然适合表格:麦芽列表是行,酒花添加时间是列;批次记录是行,状态和时间是列。BrewUI 在大部分数据展示场景里优先使用表格组件,只是在配方详情页才引入树形结构展示“配方 → 原料 → 酿造记录”的三级关系。
表格设计上我坚持了三条规则:列数量控制在五列以内,核心列固定在视图范围内;数值列靠右对齐,方便快速比较大小;表格行之间留足点击热区,避免误触相邻行。移动端表格不做横向滚动,而是采用“卡片化”展示,一行数据变成一张小卡片,展开后显示全部字段。这个决策牺牲了一点桌面端的信息密度,换来了移动端的可读性,值得。
交互上,下拉刷新、左滑删除、长按复制这些移动端习惯动作都做了接入。特别是左滑删除,在配方实验阶段频繁增删原料时,效率比点开详情再找删除按钮高出一大截。
4.3 表单校验与默认值联动
酿造现场最烦的就是表单弹出一堆报错。BrewUI 的表单校验策略是只做必要校验、不做过度限制。所有字段均可留空,但一旦填写,格式就必须正确。比如 OG 字段,如果填写了,就必须是 1.000 到 1.200 之间的数字;温度必须是 -10 到 100 之间的数字;时间必须是大于 0 的整数。
默认值联动的关键在于“风格感知”。选择配方风格为 NEIPA 时,系统自动把糖化温度默认到 66℃,煮沸时间默认到 60 分钟,苦度目标默认到 40 IBU,色度默认到 6 SRM;选择世涛则默认糖化温度 68℃,苦度默认 55 IBU,色度默认 35 SRM。这些默认值来自典型风格的核心参数区间,不是从算法里硬推出来的,有经验的酿酒师一眼就知道合理。
还有一个细节:所有默认值在用户手动修改过之后,就不再被联动覆盖。我见过一些工具,选完风格后自动填充字段,用户改了数值一失焦又被打回默认值,气得人直接关页面。BrewUI 的做法是联动只发生在风格改变的那一刻,之后的任何修改都是用户意愿,系统不干预。
5. 常见问题与排查技巧实录
5.1 糖化步进记录丢失
BrewUI 刚上线时,我遇到过最典型的问题:糖化进行到一半,添加麦芽的操作记录竟然消失了。排查后发现,原因是前端在提交记录时的网络请求超时,而当时的表单没有做本地暂存,页面一刷新就全没了。
这个问题的根子是“提交即忘”,没有本地兜底。后来我给所有关键表单都加了一层 localStorage 草稿缓存,输入内容每 5 秒自动保存,提交成功后清除。这样即使网络断了、页面崩溃,重新打开时也能一键恢复草稿。这套机制不仅解决了数据丢失,还意外降低了重复填写带来的烦躁感。
给同行的建议是:涉及关键操作的提交,一定要有某种形式的本地补偿机制。不用复杂,一个简单的时间戳草稿缓存就能避免大部分事故。
5.2 比重校准偏差
用 BrewUI 记录比重时,遇到过一个相对隐蔽的问题:手头有两个不同的比重计,温度补偿不一样,同一份麦汁测出的 OG 数值相差了 0.003。对于追求批次一致性的酿造来说,这个偏差不可忽视。
后来我在读数录入界面加了一个“温度修正”的字段,记录实测温度和仪器校准温度,系统自动按公式换算成标准温度下的比重。同时在界面显眼位置提示用户尽量固定使用同一支比重计,这样历史数据才有纵向可比性。
这个问题的教训是:数据精度高不高,很大程度上取决于采集环节的工具一致性。工具能做的,是在输入端提示和校正,但最终标准得靠人统一。我在 UI 上把“仪器型号”也做成可选字段,不同仪器之间的批次对比能直接分组查看。
5.3 移动端表单误触
移动端表单的误触问题主要来自两个地方:下拉选择器太窄,以及数字键盘弹出后挡住了底部按钮。Vant 的 Picker 默认宽度比较适合做单一选择,但酿造中的麦芽选择往往还需要附带数量输入,两个组件叠在一起,操作路径就变长了。
我的解决方案是把“选择麦芽”和“输入克数”拆成两个独立的交互步骤,麦芽搜索用全屏弹层,选中后自动跳到数量输入框并弹出数字键盘。这样每一步都有明确的视觉焦点,有效避免了误触和来回切换。另外,所有底部操作按钮都做了离底部 88px 的安全区适配,Home 键手势区域不会遮挡按钮。
这些交互细节,不实际操作几次真发现不了。我建议前端开发者做完一套界面后,自己去模拟一遍“一手拿手机、一手撒麦芽”的现场操作,感受一下哪里别扭,那基本上就是最真实的优化方向。
5.4 数据导出与备份
BrewUI 的重要数据都是本地保存,虽然用了 SQLite,但不做备份策略显然说不过去。我的处理是设置页面提供一键导出功能,把配方、批次、读数三张表序列化成 JSON 文件,用户可以手动下载到本地或网盘。
同时在后端加了一个简单的定时策略:每次应用启动时,如果距离上次全量备份超过 7 天,就自动生成一份带日期的备份文件,保存在备份目录里,保留最近 30 份。这个策略成本极低,但心里踏实很多。
还有一个容易被忽略的点:因为数据库文件本身可能就是工具的核心资产,我会建议用户在每次大型酿造活动开始前手动导出一份快照。酿到一半工具挂了不可怕,可怕的是几天酿造数据丢了没法恢复。多做一次导出,只是多一步操作,关键时刻能省去很多麻烦。
说实话,BrewUI 开发到现在,我觉得最有价值的不是它有多强的功能,而是过程中沉淀下来的这套“小工具给垂直场景留痕”的思路。从配方计算到批次记录,从传感器接入到移动端交互,每一步都不复杂,但组合起来就让酿酒这件原本靠经验和记忆的事情变得可以追溯、可以复现。如果再让我重来一次,我会在一开始就先把本地草稿缓存和数据导出做进基础框架,而不是等出了问题再补。这个内容后面我打算继续扩展的是多用户协作与配方分享模块,让身边几个酿酒朋友也能共同维护一个配方库,同时保留各自独立的批次记录。
422




被折叠的 条评论
为什么被折叠?



