宠物食谱工具的改进
今天做了一件挺完整的事:拿到一个开源小项目,修 bug、加测试、优化 UI,最后发布到华为云开发者作品展览馆。把整个过程记下来,既是复盘,也给类似流程的人一个参考。
一、起点:一个 6 个文件的小项目
事情从一个 GitCode 仓库开始:lightyearssssr/pet-food-query——一个宠物食谱查询工具。
克隆下来一看,结构非常精简:
pet-food-query/
├── app.py # Streamlit 前端,5 个页面
├── database.py # SQLite 数据层
├── seed_data.py # 22 种预填充食材数据
├── pet_food.db # SQLite 数据库文件
├── requirements.txt # 仅 streamlit>=1.30.0
└── README.md
功能挺实用:查食材安全等级、记喂食日志、按间隔天数提醒"可以喂了"还是"还要等"。预填充了 22 种狗的常见食材,包括鸡胸肉、南瓜这类安全食材,以及巧克力、葡萄这类危险食材。
但读了一遍代码,几个问题比较明显。
二、发现的问题
Bug 1:喂食提醒不区分宠物(关键 bug)
这是最严重的一个。database.py 里 get_last_feeding_date() 有一个 pet_name 参数,但 app.py 的提醒页调用时没传:
# 修复前 — 没传 pet_name
last_date_str = db.get_last_feeding_date(row["id"])
后果是什么?假设你家有旺财和咪咪两只狗,旺财 1 月 10 日吃了鸡胸肉,咪咪 1 月 25 日也吃了。查旺财的提醒时,系统取到的是所有宠物的最新喂食日期(1 月 25 日),而不是旺财自己的(1 月 10 日)。两只宠物共享一个冷却计时,提醒完全错乱。
Bug 2:搜索不支持种类筛选
seed_data.py 里所有食材的 species 都是 "dog",添加食材表单有 dog/cat 选项,但搜索页和提醒页都没有按种类过滤。你加了猫的食材,搜的时候还是全部混在一起。
Bug 3:pet_food.db 提交到了 Git
二进制数据库文件不应该进版本控制——每次运行改动都会产生 diff,仓库会越来越臃肿。而且没有 .gitignore。
其他
- 没有任何测试
- 搜索是纯
LIKE '%keyword%',粒度较粗 - UI 是 Streamlit 默认样式,功能可用但缺乏打磨
三、动手改
3.1 修 database
核心改动是给 search_ingredients() 加了 species 参数:
def search_ingredients(keyword, species=None):
query = "SELECT * FROM ingredients WHERE name LIKE ?"
params = [f"%{keyword}%"]
if species:
query += " AND species = ?"
params.append(species)
# ...
同时给 init_db() 加了 db_path 参数,方便测试时用临时数据库,不污染主库。还新增了 get_pet_names() 方法,用于提醒页的下拉框。
3.2 修 app
提醒页是改动最大的地方。加了一个宠物名选择器——如果已经有喂食记录,下拉框会列出已知宠物名;也可以手动输入新名字。选好宠物后,调用时传入 pet_name:
# 修复后 — 传入 pet_name
last_date_str = db.get_last_feeding_date(row["id"], pet_name=pet_name)
没选宠物名时,显示一个解释提示,告诉用户为什么要输入:
💡 为什么要输入宠物名?
多宠物家庭中,每只宠物对同一食材有独立的冷却周期。输入宠物名后,提醒会根据该宠物的喂食记录单独计算。
搜索页加了种类筛选下拉框(全部 / 🐶狗 / 🐱猫),食材卡片也加了种类标签显示。添加食材的表单按"分类信息 / 喂食指导 / 营养与注意"三段分组,视觉上更清晰。侧边栏加了版本号 v1.1.0。
3.3 加 .gitignore,移除 pet_food.db
git rm --cached pet_food.db # 从 git 移除,保留本地文件
.gitignore 内容:
pet_food.db
__pycache__/
*.pyc
.venv/
.env
.pytest_cache/
3.4 写测试
用 pytest 写了 24 个单元测试,覆盖 database.py 的所有核心函数。每个测试用 tmp_path fixture 创建临时数据库,互不影响。
其中最关键的一个是 test_pet_name_filter_isolates_pets——专门针对那个 bug 的回归测试:
def test_pet_name_filter_isolates_pets(self, tmp_db):
"""旺财 1/10 喂了鸡胸肉,咪咪 1/25 也喂了。
查旺财的上次喂食日期应返回 1/10,不是 1/25。"""
# ...
result_wangcai = db.get_last_feeding_date(ing_id, pet_name="旺财")
assert result_wangcai == "2025-01-10"
result_mimi = db.get_last_feeding_date(ing_id, pet_name="咪咪")
assert result_mimi == "2025-01-25"
跑一下:
============================== 24 passed in 0.38s ==============================
全绿。
五、回顾
今天的工作量不大,但流程很完整:
| 阶段 | 产出 |
|---|---|
| 代码审查 | 发现 4 个问题(1 个关键 bug + 3 个工程问题) |
| Bug 修复 | 喂食提醒按宠物名独立计算、搜索加种类筛选 |
| 测试 | 24 个单元测试,0.38s 全绿 |
| UI 优化 | 搜索筛选、提醒页宠物选择器、表单分组、表格列宽 |
| 工程化 | .gitignore、移除二进制文件、版本号 |
| 发布 | 封面图 + 架构图 + 详情文章 → 华为云作品展览馆 |
几个感受:
-
那个 pet_name bug 很典型——参数定义了但调用方没传,类型检查也查不出来。如果没有多宠物的实际场景,可能永远不会发现。写测试时专门针对这个场景写了回归测试,以后改代码也不怕再破。
-
发布流程的门禁设计值得学习——字体、编码、字形验证,三道关卡层层拦截。在 arm64 Linux 上跑 Playwright 截图确实有不少坑(字体安装、fallback build),但门禁保证了最终产出不会有乱码或豆腐块。
-
接口细节容易搞错——POST 还是 GET,路径有没有后缀,参数在 body 还是 header。看文档要仔细,猜是不行的。
如果你也在养宠物,或者对 Streamlit 小工具感兴趣,欢迎来看看。
- 点赞
- 收藏
- 关注作者
评论(0)