用Python+Flask+SQLite实现家庭食材库存管理与保质期预警系统
暴雨天空调房里开着灯冰箱里那盒麻辣小龙虾和一瓶自制青提啤饮最终还是没忍住。本来只是想“浅尝一口”结果虾壳堆成小山青提啤饮也见了底。等到家人推门进来场面一度非常尴尬。也就是那天晚上我意识到家里的食材库存一直处于“凭记忆管理”的状态什么东西还有多少、什么时候过期、被谁吃掉了完全说不清楚。与其继续靠脑补不如动手写一套家庭食材库存管理系统。这篇文章会从零开始带你用 Python 实现一个包含库存管理、保质期预警、库存不足提醒的 Web 小系统。技术栈选用 Flask SQLite APScheduler数据存储简单代码量可控适合新手学习也适合有 Python 基础的开发者快速搭建自己的工具。读完这篇文章你会掌握如何用 SQLite 设计食材库存表结构如何用 Flask 写增删改查接口如何用 APScheduler 定时扫描临期食材如何把“有人偷吃了食材”这类操作变成可追踪的记录如何排查定时任务、端口占用、中文乱码等常见问题。整体流程会拆得很细每个关键步骤都会解释为什么这么做。你不需要提前准备复杂环境只要电脑上能跑 Python 3就能跟着一步步完成。1. 为什么需要一个家庭食材库存管理系统1.1 “深夜偷吃”暴露的真实问题那盒麻辣小龙虾并不是当天买的而是周末囤货时放进冰箱的。青提啤饮也是自己批量调制后冷藏的。问题在于这些食材没有记录在案没人知道冰箱里还剩多少也没人记得保质期到哪一天。当“偷吃”发生后真正的麻烦是家庭库存数据出现偏差。下次做饭时可能打开冰箱才发现食材已经用完或者过期了。类似场景在开发里也有对应概念缺少操作日志、没有库存快照、依赖人工记忆维护状态。只要把家里想象成一个微型仓库食材就是 SKU冰箱就是库房吃的过程就是出库操作。所以这个项目的核心并不是“防偷吃”而是建立一套可查询、可追踪、可预警的食材库存台账。1.2 系统能解决哪些场景家庭食材管理看起来简单实际场景比想象中多食材入库买了菜、肉类、饮料后登记名称、数量、保质期食材出库吃掉、用完、扔掉的食材需要扣减库存临期提醒牛奶、豆腐、熟食这类短保食材需要提前几天提醒库存不足提醒鸡蛋少于 5 个、饮用水少于 2 瓶时自动提示补货操作追踪记录谁在什么时间添加、使用了什么食材避免“库存神秘消失”。这个系统不一定非要跑在服务器上普通电脑甚至树莓派都能运行。只要把服务启动起来家庭成员打开浏览器就能查看库存、登记出入库。1.3 技术选型为什么是 Flask SQLite APScheduler选型时我优先考虑“低门槛、少依赖、能跑通”。最终确定 Flask SQLite APScheduler原因如下Flask 轻量一个 Python 文件就能启动 Web 服务模板引擎自带适合快速开发SQLite 是 Python 内置支持的数据库不需要额外安装 MySQL数据保存在单个.db文件中备份方便APScheduler 是成熟的 Python 定时任务库支持 cron 表达式适合每天定时扫描临期食材整个项目用到的依赖只有 Flask 和 APScheduler安装成本很低。如果你后续想把它部署到云服务器这套代码也能平滑迁移只需要把 SQLite 换成 MySQL再加入简单的登录认证即可。2. 系统整体设计与技术准备2.1 功能拆分在动手写代码前先明确系统要做什么。家庭食材库存管理系统按功能可以拆成四部分模块职责关键功能数据层使用 sqlite3 操作 SQLite 数据库建表、增删改查、库存统计业务层封装食材和提醒的业务逻辑入库、出库、临期检测、低库存检测Web 层使用 Flask 提供页面和接口展示库存、提交新食材、执行出库定时任务使用 APScheduler 定时执行每天扫描临期食材、低库存食材建议家庭场景下所有功能都通过 Web 页面操作这样手机浏览器也能访问。命令行方式适合开发调试不必作为主入口。2.2 环境准备本项目需要的运行环境如下Python 3.10 及以上版本pip 包管理工具Flask 2.xAPScheduler 3.x操作系统不限Windows / macOS / Linux 均可。如果你的 Python 版本较低建议先升级到 3.10 再继续避免语法兼容问题。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.3 项目结构建议按照下面的目录结构组织代码后续扩展会方便很多kitchen-inventory/ ├── app.py # Flask 主程序 ├── models.py # 数据库操作模块 ├── scheduler.py # 定时任务模块 ├── requirements.txt # 依赖列表 ├── templates/ │ └── index.html # 库存管理页面 └── kitchen_inventory.db # SQLite 数据库文件自动生成这里把数据库操作单独放到 models.pyFlask 路由放在 app.py定时任务放在 scheduler.py职责划分清楚不会出现几千行堆在一个文件里的情况。2.4 依赖安装在项目根目录下创建requirements.txt内容如下flask2.2 apscheduler3.10,4.0然后执行安装命令pip install -r requirements.txt如果你使用的是虚拟环境建议先创建并激活虚拟环境再安装依赖python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install -r requirements.txt依赖安装完成后可以通过 Python 交互命令验证 Flask 是否可用python -c import flask; print(flask.__version__)能正常输出版本号说明环境没问题。3. 数据库设计与模型实现3.1 食材表设计家庭库存的食材信息相对简单但为了保证可用性我设计了以下字段字段类型说明idINTEGER主键自增nameTEXT食材名称categoryTEXT分类如海鲜、饮品、蔬菜quantityREAL当前数量unitTEXT单位如只、瓶、盒purchase_dateTEXT购买日期expiry_dateTEXT过期日期min_quantityREAL库存预警阈值created_atTEXT创建时间min_quantity是低库存阈值。比如鸡蛋设置 5 个当数量小于等于 5 时系统就会触发“库存不足”提醒。日期统一使用YYYY-MM-DD格式方便比较大小。3.2 操作记录表设计为了追踪“到底是谁把小龙虾吃掉了”还需要一张操作记录表。每次入库、出库、修改都写入一条记录形成完整时间线。字段类型说明idINTEGER主键自增food_idINTEGER食材 IDactionTEXT操作类型ADD / USE / UPDATE / DELETEquantityREAL变更数量noteTEXT备注created_atTEXT操作时间这样当库存数量对不上时可以通过操作记录倒查是哪一步出了问题。3.3 初始化数据库在models.py中用原生 sqlite3 实现数据库操作。为了避免 SQL 注入所有 SQL 语句都使用参数化?占位符。# 文件路径models.py import sqlite3 from datetime import datetime, timedelta DB_PATH kitchen_inventory.db class FoodStore: def __init__(self, db_pathDB_PATH): self.db_path db_path self.init_db() def get_connection(self): conn sqlite3.connect(self.db_path) conn.row_factory sqlite3.Row return conn def init_db(self): with self.get_connection() as conn: conn.execute( CREATE TABLE IF NOT EXISTS foods ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT DEFAULT 其他, quantity REAL DEFAULT 0, unit TEXT DEFAULT 份, purchase_date TEXT, expiry_date TEXT, min_quantity REAL DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) conn.execute( CREATE TABLE IF NOT EXISTS operations ( id INTEGER PRIMARY KEY AUTOINCREMENT, food_id INTEGER, action TEXT, quantity REAL DEFAULT 0, note TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ) conn.execute( CREATE TABLE IF NOT EXISTS alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, food_id INTEGER, alert_type TEXT, message TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) )这里使用sqlite3.connect建立连接row_factory sqlite3.Row可以让查询结果像字典一样通过字段名访问。with块会自动提交事务异常时回滚。4. 核心功能实现库存管理与保质期预警4.1 添加食材添加食材是系统的基础功能。除了保存食材名称、数量、保质期外还要往操作记录表写入一条 ADD 记录方便追溯。def add_food(self, name, category, quantity, unit, expiry_date, purchase_dateNone, min_quantity1): if purchase_date is None: purchase_date datetime.now().strftime(%Y-%m-%d) with self.get_connection() as conn: cursor conn.execute( INSERT INTO foods (name, category, quantity, unit, purchase_date, expiry_date, min_quantity) VALUES (?, ?, ?, ?, ?, ?, ?) , (name, category, quantity, unit, purchase_date, expiry_date, min_quantity) ) food_id cursor.lastrowid conn.execute( INSERT INTO operations (food_id, action, quantity, note) VALUES (?, ADD, ?, ?) , (food_id, quantity, f新增食材{name}) ) return food_id这里把purchase_date默认设为当天日期。cursor.lastrowid可以拿到新插入食材的自增 ID便于写入关联操作记录。4.2 查询库存与排序查询库存时按保质期剩余天数排序临期食材排在最前面。这样打开页面就能优先看到哪些食材需要处理。def list_foods(self): with self.get_connection() as conn: rows conn.execute( SELECT * FROM foods ORDER BY expiry_date IS NULL, expiry_date ASC ).fetchall() return [dict(row) for row in rows]SQL 中expiry_date IS NULL的排序技巧可以把没有设置保质期的食材放在最后有保质期的按日期从近到远排列。4.3 临期与低库存检测临期检测是系统的核心亮点。先计算从今天到保质期还剩多少天然后筛选出小于等于预警天数的食材。def get_expiring_foods(self, days3): today datetime.now().strftime(%Y-%m-%d) target_date (datetime.now() timedelta(daysdays)).strftime(%Y-%m-%d) with self.get_connection() as conn: rows conn.execute( SELECT * FROM foods WHERE expiry_date IS NOT NULL AND expiry_date ? AND expiry_date ? ORDER BY expiry_date ASC , (today, target_date) ).fetchall() return [dict(row) for row in rows]低库存检测则通过比较当前数量和预警阈值找出需要补货的食材。def get_low_stock_foods(self): with self.get_connection() as conn: rows conn.execute( SELECT * FROM foods WHERE quantity min_quantity ORDER BY quantity ASC ).fetchall() return [dict(row) for row in rows]4.4 出库与删除出库操作对应“食材被吃掉”的场景。每次出库都要扣减数量并记录 USE 操作。def use_food(self, food_id, quantity, note食材出库): with self.get_connection() as conn: food conn.execute( SELECT * FROM foods WHERE id ?, (food_id,) ).fetchone() if not food: return False new_quantity food[quantity] - quantity if new_quantity 0: new_quantity 0 conn.execute( UPDATE foods SET quantity ? WHERE id ?, (new_quantity, food_id) ) conn.execute( INSERT INTO operations (food_id, action, quantity, note) VALUES (?, USE, ?, ?) , (food_id, quantity, note) ) return True删除操作会同时删除食材和对应的操作记录。生产环境中一般不提倡物理删除但家庭系统为了操作方便可以直接删除。def delete_food(self, food_id): with self.get_connection() as conn: conn.execute(DELETE FROM foods WHERE id ?, (food_id,)) conn.execute(DELETE FROM operations WHERE food_id ?, (food_id,)) conn.execute(DELETE FROM alerts WHERE food_id ?, (food_id,)) return True5. 基于 Flask 的 Web 管理界面5.1 Flask 路由设计Flask 负责提供 Web 页面和表单提交接口。首页展示所有食材、临期食材、低库存食材以及最近提醒。# 文件路径app.py from flask import Flask, render_template, request, redirect, url_for from models import FoodStore app Flask(__name__) store FoodStore() app.route(/) def index(): foods store.list_foods() expiring store.get_expiring_foods(days3) low_stock store.get_low_stock_foods() alerts store.get_alerts(limit20) return render_template( index.html, foodsfoods, expiringexpiring, low_stocklow_stock, alertsalerts ) app.route(/add, methods[POST]) def add(): name request.form.get(name) category request.form.get(category, 其他) quantity float(request.form.get(quantity, 0)) unit request.form.get(unit, 份) expiry_date request.form.get(expiry_date) or None min_quantity float(request.form.get(min_quantity, 1)) store.add_food(name, category, quantity, unit, expiry_date, min_quantitymin_quantity) return redirect(url_for(index)) app.route(/use/int:food_id, methods[POST]) def use(food_id): quantity float(request.form.get(quantity, 1)) store.use_food(food_id, quantity) return redirect(url_for(index)) app.route(/delete/int:food_id, methods[POST]) def delete(food_id): store.delete_food(food_id) return redirect(url_for(index)) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这里把debugFalse是为了避免 Flask 调试模式下的 reloader 与定时任务冲突。如果你要调试可以单独开 debug但要注意定时任务重复执行的问题。5.2 页面模板实现模板文件放在templates/index.html使用简单 CSS不依赖外部 CDN内网环境也能正常显示。!-- 文件路径templates/index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title家庭食材库存管理/title style body { font-family: Microsoft YaHei, sans-serif; max-width: 1200px; margin: 20px auto; padding: 0 20px; } table { border-collapse: collapse; width: 100%; margin-bottom: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: center; } th { background-color: #f5f5f5; } .expired { color: red; font-weight: bold; } .warning { color: orange; } form { display: inline; } .card { border: 1px solid #eee; padding: 16px; border-radius: 8px; margin-bottom: 20px; } /style /head body h1家庭食材库存管理/h1 div classcard h2新增食材/h2 form action/add methodpost 名称: input typetext namename required 分类: input typetext namecategory placeholder海鲜/饮品/蔬菜 数量: input typenumber namequantity step0.1 value1 required 单位: input typetext nameunit placeholder份/瓶/盒 保质期: input typedate nameexpiry_date 预警数量: input typenumber namemin_quantity step0.1 value1 button typesubmit新增/button /form /div h2临期食材3 天内/h2 table trth名称/thth数量/thth保质期/thth状态/th/tr {% for food in expiring %} tr td{{ food.name }}/td td{{ food.quantity }} {{ food.unit }}/td td{{ food.expiry_date }}/td td classwarning即将过期/td /tr {% else %} trtd colspan4暂无临期食材/td/tr {% endfor %} /table h2全部食材/h2 table tr th名称/thth分类/thth数量/thth单位/th th购买日期/thth保质期/thth操作/th /tr {% for food in foods %} tr td{{ food.name }}/td td{{ food.category }}/td td{{ food.quantity }}/td td{{ food.unit }}/td td{{ food.purchase_date }}/td td{{ food.expiry_date }}/td td form action/use/{{ food.id }} methodpost styledisplay:inline; input typenumber namequantity value1 step0.1 stylewidth:60px; button typesubmit出库/button /form form action/delete/{{ food.id }} methodpost styledisplay:inline; button typesubmit删除/button /form /td /tr {% else %} trtd colspan7暂无食材/td/tr {% endfor %} /table h2库存不足/h2 ul {% for food in low_stock %} li{{ food.name }}剩余 {{ food.quantity }} {{ food.unit }}预警值 {{ food.min_quantity }}/li {% else %} li暂无库存不足/li {% endfor %} /ul h2最近提醒/h2 ul {% for alert in alerts %} li{{ alert.created_at }} - {{ alert.message }}/li {% else %} li暂无提醒/li {% endfor %} /ul /body /html模板中使用了 Flask 自带的 Jinja2 语法{% for %}和{% else %}配合使用可以在列表为空时显示提示文案。5.3 运行与验证在项目根目录执行python app.py浏览器访问http://127.0.0.1:5000如果是在局域网其他设备访问需要使用电脑的 IP例如http://192.168.1.100:5000。首次访问时页面为空可以通过页面表单新增一条小龙虾食材再新增一条青提啤饮分别设置不同的保质期验证入库功能是否正常。6. 定时任务与提醒机制6.1 APScheduler 集成APScheduler 是 Python 生态中比较成熟的定时任务库。这里使用BackgroundScheduler它会在后台线程中执行任务不影响 Flask 主进程。# 文件路径scheduler.py import logging from apscheduler.schedulers.background import BackgroundScheduler from datetime import datetime logger logging.getLogger(__name__) def check_inventory(store): 扫描库存并记录提醒。 today datetime.now().strftime(%Y-%m-%d) expiring_foods store.get_expiring_foods(days3) for food in expiring_foods: message f食材 [{food[name]}] 将在 {food[expiry_date]} 过期剩余 {food[quantity]} {food[unit]} store.add_alert(food[id], EXPIRING, message) logger.warning(message) low_stock_foods store.get_low_stock_foods() for food in low_stock_foods: message f食材 [{food[name]}] 库存不足当前 {food[quantity]} {food[unit]}预警值 {food[min_quantity]} store.add_alert(food[id], LOW_STOCK, message) logger.warning(message) def create_scheduler(store): scheduler BackgroundScheduler(timezoneAsia/Shanghai) scheduler.add_job( check_inventory, triggercron, hour20, minute0, args[store], idinventory_check_job, replace_existingTrue, ) return scheduler这里使用cron触发器设定每天 20:00 执行一次库存检查。如果你想测试可以临时把时间改成每分钟执行一次比如minute*。6.2 提醒任务逻辑提醒逻辑分为两步查询临期食材和低库存食材将提醒内容写入alerts表同时输出到日志。在models.py中补充add_alert和get_alerts方法def add_alert(self, food_id, alert_type, message): with self.get_connection() as conn: conn.execute( INSERT INTO alerts (food_id, alert_type, message) VALUES (?, ?, ?) , (food_id, alert_type, message) ) def get_alerts(self, limit20): with self.get_connection() as conn: rows conn.execute( SELECT * FROM alerts ORDER BY id DESC LIMIT ? , (limit,) ).fetchall() return [dict(row) for row in rows]把提醒记录到数据库的好处是即使错过日志也能在 Web 页面看到历史提醒方便回溯。6.3 与 Flask 一起启动在app.py中启动定时任务。注意要放在app.run()之前并且关闭 Flask debug 模式避免 reloader 导致任务重复执行。# 文件路径app.py补充部分 from scheduler import create_scheduler if __name__ __main__: scheduler create_scheduler(store) scheduler.start() try: app.run(host0.0.0.0, port5000, debugFalse) except (KeyboardInterrupt, SystemExit): scheduler.shutdown()这样 Flask 服务和定时任务就在同一个进程里运行。如果后续想把定时任务拆成独立服务也可以把 scheduler 单独部署。7. 完整代码运行演示7.1 初始化数据与启动服务把所有代码准备好后先运行一次初始化脚本验证数据库创建是否正常。python -c from models import FoodStore; FoodStore(); print(数据库初始化完成)正常会输出“数据库初始化完成”并且项目目录下生成kitchen_inventory.db文件。7.2 添加食材测试为了还原“暴雨夜偷吃小龙虾”的场景可以先用命令行批量添加几条测试数据。python -c from models import FoodStore store FoodStore() store.add_food(麻辣小龙虾, 海鲜, 3, 盒, 2026-12-20, min_quantity1) store.add_food(自制青提啤饮, 饮品, 6, 瓶, 2026-12-25, min_quantity2) store.add_food(牛奶, 乳品, 4, 盒, 2026-12-10, min_quantity2) print(测试数据添加完成) 这里给小龙虾设置 3 盒青提啤饮 6 瓶牛奶 4 盒。执行后打开数据库可以看到 3 条食品记录。7.3 验证预警输出启动 Flask 服务后手动调用库存检查函数观察提醒是否生成。python -c from models import FoodStore from scheduler import check_inventory store FoodStore() check_inventory(store) alerts store.get_alerts() for alert in alerts: print(alert[message]) 如果当前日期接近牛奶保质期或者青提啤饮数量低于预警值就会输出对应的提醒内容。这就是临期预警和低库存预警的核心验证方式。如果想要更完整地模拟“偷吃”过程可以在页面把麻辣小龙虾出库 1 盒。执行后食材数量会从 3 变成 2操作记录里会增加一条 USE 记录。8. 常见问题与排查思路家庭项目中常见的坑主要集中在依赖、端口、中文编码和定时任务几个方面。我把高频问题整理成表格方便你按图索骥。问题现象常见原因解决思路启动报错 ModuleNotFoundError: No module named flask未安装 Flask 或虚拟环境未激活执行pip install -r requirements.txt确认激活虚拟环境5000 端口被占用其他进程占用了端口修改app.run(port5001)或使用lsof -i:5000查看占用进程中文显示乱码控制台编码不是 UTF-8Windows 执行chcp 65001或设置PYTHONIOENCODINGutf-8定时任务重复执行Flask debug 模式 reloader 启动了两次进程关闭 debug或设置use_reloaderFalse日期比较结果不正确日期字段格式不统一统一使用YYYY-MM-DD字符串格式不要混用日期类型数据库文件找不到运行目录不对使用绝对路径DB_PATH或者在项目根目录运行脚本出库数量超了库存前端没有做数量限制后端增加判断new_quantity 0时归零并记录告警如果遇到“点击出库后页面没变化”优先检查表单提交的food_id是否正确再看use_food是否真的修改了数据库。可以临时打印food_id和quantity来定位问题。9. 最佳实践与工程建议9.1 数据安全与备份SQLite 只有一个数据库文件备份非常方便。建议把kitchen_inventory.db加入定时备份任务例如每天凌晨复制一份到备份目录。cp kitchen_inventory.db backups/kitchen_inventory_$(date %Y%m%d).db如果是 Windows 环境可以使用计划任务配合copy命令。修改数据库表结构前一定先备份原始文件避免误删数据。9.2 代码组织与扩展方向当前代码虽然是小项目但已经划分了 models、app、scheduler 三个模块。如果你要继续扩展建议优先考虑下面几个方向加入用户登录和操作人字段解决“到底是谁吃掉的”问题使用 SQLAlchemy 替代原生 sqlite3方便切换 MySQL加入 Excel 批量导入功能一键录入一周采购清单接入企业微信、钉钉或邮件通知替代日志输出加入扫码功能通过条形码快速定位食材。家庭系统不需要一上来就设计成微服务先把核心链路跑通再根据真实使用场景迭代。9.3 安全边界与最小权限虽然这是家庭内部工具也要注意基本安全不要直接在公网暴露端口建议通过内网访问或使用带认证的反向代理Web 表单提交的参数要做类型校验防止非法输入导致程序崩溃所有 SQL 都使用参数化查询避免 SQL 注入风险删除操作建议增加确认弹窗防止误删如果多人使用最好给每个成员分配独立记录标识方便追溯操作来源。另外定时任务中涉及库存写操作时要考虑并发问题。家庭场景并发不高但如果你将来部署到服务器建议给数据库写操作加锁或使用队列。最后想说的是这个系统的核心价值不是代码本身而是把生活中的小需求拆成数据模型、业务逻辑、展示层、定时任务四个部分。你不需要一开始就做大而全的平台先把食材入库、临期预警、库存不足这三件事跑通后续再慢慢扩展就能形成一个很顺手的工具。下次冰箱里再出现“神秘消失”的食材时打开系统的操作记录至少能看到一条清晰的出库时间线。