news 2026/9/3 10:45:45

从16元抿节面套餐学数据库设计:SKU、配料与续面规则建模

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从16元抿节面套餐学数据库设计:SKU、配料与续面规则建模

一家开在河北博物院西侧范光胡同的小店,把“老陕北抿节”做成了类似套餐的形式:16 元的抿节面,默认搭配 15 种小料,并且把免费续面当作基础权益。从产品运营角度看,这是把“吃面”做成了“规则组合”;从系统开发角度看,这件事意味着菜单不只是单纯的商品列表,而是一组可以配置的 SKU、配料关系和续面规则。如果门店以后要上线扫码点餐、收银结算、会员储值或者外卖单量统计,最先要解决的都不是前端页面,而是“一碗面到底包含了什么、能续几次、多拿小料怎么计价”这些底层数据问题。

下面我会以这家店的一句话宣传语为业务背景,从零设计一套简单但可运行的点餐与续面规则模型。技术栈选择 Python 3 + SQLite,不需要额外安装数据库服务。整个案例会包含表结构、初始化数据、下单、支付、续面校验和常见报错排查,读者可以在本地直接跑通,再把同样的思路迁移到 MySQL、PostgreSQL 或者其他后端项目里。

有一点需要提前说明:这里出现的“15种小料明细”“续面次数”都是示例数据,用来演示建模思路。真实门店的配料表、续面次数和门店规则,应该以门店实际运营要求为准。

1. 先理解“16元的抿节面”为什么不能当成普通商品表

1.1 表面是套餐,底层是“规则组合”

传统的商品表一般只有商品编号、名称、价格、库存几个字段。对于一罐饮料、一袋零食,这种做法完全够用,但面对“16 元抿节面 + 15 种小料 + 免费续面”时就不够用了,因为这一句话里包含多种事实:

  • 抿结面是一个基础 SKU,基础售价是 1600 分。
  • 这个 SKU 默认包含 15 种小料,这里的小料不是都拆分单独收费。
  • 顾客吃完基础份量后可以免费续面,续面次数和间隔规则需要单独记录。
  • 如果某些小料当日没有,或者小料需要额外加价,规则又不一样。

所以,“16元”不是商品名称的一部分,而是某个 SKU 的当前价格;“15种小料”不是一句描述,而是商品和配料之间的多对多关系;“免费续面”更不是备注,而是一张需要被程序校验的权益规则。

1.2 系统里应该记住哪些业务事实

在设计数据表之前,最好把需求拆成系统能够处理的事实。以这家店为例,可以拆成四类。

第一类是基础商品,也就是“抿节系列”。它只表示门店在卖这个品类,不应直接绑定价格。比如同样是抿节,可以衍生出基础款、加牛肉款、大份款,未来还能加辣度标签。

第二类是具体 SKU。比如“16 元抿节基础套餐”这个可售版本,它有确定价格、是否可续面、是否上架。以后门店调价,也不应该直接改商品表,而是可以给菜单 SKU 增加生效时间段,避免历史订单对不上价格。

第三类是配料关系。15 种小料需要单独建立一张配料表,再用关系表把它们绑定到某个 SKU 下,才能回答“这份套餐为什么卖 16 元”和“顾客选择的套餐里有哪些默认配料”。

第四类是续面规则。续面免费不等于无限制纵容,门店通常会在桌台、排队、出餐效率之间做取舍。所以最好把最大次数、间隔时间、是否仅限本单、是否允许外带等规则独立成表,而不是写死在代码里。

一个合理的判断标准是:如果内容未来可能因为日期、门店、套餐版本而变化,就应该把它做成数据;如果永远不变,写成常量也不会有太大问题。餐饮菜单几乎天天变,所以只要出现套餐组合,尽量走配置表。

1.3 最终的数据主线

我会用一条主线来展开:菜单表只描述“有什么可卖”,SKU 表描述“某个可售版本卖多少钱、能不能续”,配料关系表描述“套餐里包含哪些小料”,订单与续面记录表描述“顾客买了几份、实际续了几次面”。这样以后不管门店把 16 元改成 18 元,还是把小料从 15 种调整为 12 种,都只需要调整数据,不需要改业务代码。

这套思路也可以平移到奶茶店的小料加料、火锅店的蘸料套餐、快餐店的“加量续饭”业务。数据结构比代码更能决定系统的扩展边界,这也是为什么很多点餐系统会把菜单做成“商品 + 规格 + 加料 + 规则”的组合。

2. 环境准备:先跑通最小 Python + SQLite 项目

2.1 技术选型为什么要用 Python 和 SQLite

Python 自带的sqlite3模块不需要额外安装,非常适合做数据建模演示。SQLite 是文件型数据库,所有数据都存放在一个.db文件里,方便本地快速验证。

生产环境如果并发量高,一般会更建议使用 MySQL 或者 PostgreSQL。两者的表结构可以按同样思路设计,只是连接方式、事务隔离级别和部署方式会有差异。本文先解决“规则和逻辑能不能跑通”,不追求一上来就搭大型微服务。

2.2 版本和环境检查

本地建议使用 Python 3.8 以上版本。先打开终端确认版本:

python --version

能正常输出版本号即可。如果电脑上同时安装了 Python 2,可能需要使用python3命令:

python3 --version

本示例只使用标准库,因此不需要pip install任何第三方包。确认sqlite3模块可用:

python -c "import sqlite3; print(sqlite3.sqlite_version)"

输出 SQLite 版本号就说明环境正常。接下来创建一个目录,用来保存数据库文件、建表脚本和业务代码。

mkdir noodle_shop && cd noodle_shop

2.3 建议的目录结构

为了让“结构清晰”而且“可以逐步执行”,推荐按下面的方式组织文件:

noodle_shop/ ├── schema.sql # 建表语句 ├── seed.sql # 准备菜单与小料数据 ├── noodle_shop.py # 核心下单/支付/续面逻辑 └── demo_run.py # 运行演示

如果只是在本地练习,也可以把建表和初始化数据合并成一个 Python 脚本。但作为可以长期维护的项目,建议把 SQL 和业务代码分开,这样 DBA 和开发人员都能快速看懂变更点。

3. 数据库设计:从“商品”到“套餐规则”的拆表过程

3.1 核心表拆成 7 张

我先给出最终设计的 7 张核心数据表,它们是这套点餐规则的基础:

表名作用关键字段
menu_product商品大类,比如“抿节系列”id, name, category, enabled
menu_sku可售 SKU,比如“16元抿节基础套餐”id, product_id, sku_name, price_cents, refillable
ingredient小料/配料字典id, name, category, stock_unit, track_stock
sku_ingredientSKU 与配料的关联关系id, sku_id, ingredient_id, required
refill_rule续面规则id, sku_id, max_refill_times, interval_minutes
purchase_order订单主表id, order_no, status, table_no, total_price_cents
order_item订单明细id, order_id, sku_id, quantity, unit_price_cents, line_price_cents
refill_record续面记录id, order_item_id, refill_time

商品和 SKU 分表,是为了避免把“抿节”和“16元抿节套餐”混在一个字段里。以后门店如果出“18 元加牛肉抿节套餐”,不需要修改基础商品表,只需要在 SKU 表新增一行。

3.2 建表 SQL 与字段解释

schema.sql内容如下:

CREATE TABLE IF NOT EXISTS menu_product ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT NOT NULL DEFAULT '面食', enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS menu_sku ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL REFERENCES menu_product(id), sku_name TEXT NOT NULL, price_cents INTEGER NOT NULL CHECK (price_cents >= 0), refillable INTEGER NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS ingredient ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, stock_unit TEXT NOT NULL DEFAULT '份', track_stock INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS sku_ingredient ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku_id INTEGER NOT NULL REFERENCES menu_sku(id), ingredient_id INTEGER NOT NULL REFERENCES ingredient(id), required INTEGER NOT NULL DEFAULT 1, UNIQUE (sku_id, ingredient_id) ); CREATE TABLE IF NOT EXISTS refill_rule ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku_id INTEGER NOT NULL UNIQUE REFERENCES menu_sku(id), max_refill_times INTEGER, interval_minutes INTEGER NOT NULL DEFAULT 0, only_original_order INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS purchase_order ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT NOT NULL UNIQUE, status TEXT NOT NULL DEFAULT 'pending', table_no TEXT, total_price_cents INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS order_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES purchase_order(id), sku_id INTEGER NOT NULL REFERENCES menu_sku(id), quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0), unit_price_cents INTEGER NOT NULL CHECK (unit_price_cents >= 0), line_price_cents INTEGER NOT NULL CHECK (line_price_cents >= 0) ); CREATE TABLE IF NOT EXISTS refill_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_item_id INTEGER NOT NULL REFERENCES order_item(id), operator TEXT, refill_time TEXT NOT NULL DEFAULT (datetime('now')) );

这里有两个容易忽略的设计点。

第一,价格存储的是price_cents,单位是“分”,不是“元”。这一点的意义会在后面排错部分专门展开。第二,refill_rule.max_refill_times使用NULL表示不限次数,使用数字表示最多可以续几次。比如NULL表示免费无限续,2表示最多续两次。要表示不允许续面,可以直接不给该 SKU 插入续面规则,或者把menu_sku.refillable设置为 0。

3.3 为什么不能把价格直接存成浮点数

在数据库里存金额,常见错误是使用FLOATREAL。由于二进制浮点数无法精确表示所有十进制小数,订单总金额、退款金额在特定数字下会出现类似15.999999的结果。虽然展示时可以格式化,参与累计、对账时容易出问题。

所以这里价格全部使用整数分。Java 里可以用long或者BigDecimal,Python 里可以直接用整数字段,对外输出时再转成元。如果数据库本身是 MySQL,对应字段可以用DECIMAL(10,2),但更稳妥的做法依然是存分或存DECIMAL(12,2)并且应用层用定点数处理。

3.4 续面规则为什么单独建表

如果只把refillable字段写在菜单 SKU 上,只能表达“能不能续面”,无法表达“续几次”“间隔多久”“是否限定本桌订单”。把这些维度放进refill_rule表,后续要调整“新店允许续两次,老店由于出餐压力只允许续一次”时,就可以通过不同 SKU 关联不同规则实现。

比如:

SKU:16元抿节基础套餐 max_refill_times = NULL interval_minutes = 15

这句话表达的是“基础套餐免费续面不限次数,但两次续面之间至少间隔 15 分钟”。间隔分钟主要用于防止顾客在同一分钟内连续点击,实际场景里店员仍然需要人工判断桌台情况。

4. 初始化菜单:塞入产品、SKU 和 15 种小料

4.1 初始化商品与 SKU

下面用seed.sql准备最基础的菜单数据:

INSERT INTO menu_product (id, name, category) VALUES (1, '抿节系列', '面食'); INSERT INTO menu_sku ( id, product_id, sku_name, price_cents, refillable ) VALUES ( 1, 1, '抿节基础套餐(含15种小料)', 1600, 1 ); INSERT INTO refill_rule ( sku_id, max_refill_times, interval_minutes, only_original_order ) VALUES ( 1, NULL, 15, 1 );

这里把“16 元”翻译成了1600分,把“免费续面”翻译成了refillable=1+max_refill_times=NULL。要注意,refill_rule里默认间隔 15 分钟是为了演示,不代表门店真是这样规定。

4.2 准备 15 种小料和套餐绑定关系

配料表的数据来自菜单实际展示。为了让例子贴近“15 种小料”这个约束,我用一组常见配料做演示:

INSERT INTO ingredient (id, name, category, track_stock) VALUES (1, '香醋', '调味', 0), (2, '蒜泥', '调味', 0), (3, '辣椒油', '调味', 0), (4, '芝麻盐', '干料', 0), (5, '酸豆角', '咸菜', 1), (6, '咸菜丁', '咸菜', 1), (7, '萝卜丝', '配菜', 1), (8, '葱碎', '配菜', 0), (9, '香菜', '配菜', 1), (10, '韭花', '咸菜', 0), (11, '干椒碎', '干料', 0), (12, '花生碎', '干料', 1), (13, '黄瓜丝', '配菜', 1), (14, '豆芽', '配菜', 1), (15, '熟芝麻', '干料', 0);

前文说过这是演示数据。如果实际门店没有某些配料,直接删除对应行即可。track_stock表示是否需要精确扣库存:像自助台小料很难做到“每一筷子都扣一次库存”,所以很多门店会设置成 0;像单独按份出餐的配菜,可以设置成 1。

下面绑定套餐和 15 种小料的关系:

INSERT INTO sku_ingredient (sku_id, ingredient_id, required) SELECT 1, id, 1 FROM ingredient ORDER BY id;

执行之后,sku_id=1的抿节基础套餐就关联到了所有启用状态的配料。required=1表示这是默认组成,不需要顾客额外确认也能上桌。

4.3 验证初始化结果

在终端执行:

sqlite3 noodle_shop.db < schema.sql sqlite3 noodle_shop.db < seed.sql

如果没有命令行sqlite3,也可以用 Python 的sqlite3模块执行:

import sqlite3 conn = sqlite3.connect("noodle_shop.db") with open("schema.sql", "r", encoding="utf-8") as f: conn.executescript(f.read()) with open("seed.sql", "r", encoding="utf-8") as f: conn.executescript(f.read()) conn.commit() conn.close()

执行后可以查询:

SELECT s.id, s.sku_name, s.price_cents, COUNT(si.id) AS ingredient_count FROM menu_sku s LEFT JOIN sku_ingredient si ON si.sku_id = s.id WHERE s.id = 1 GROUP BY s.id, s.sku_name, s.price_cents;

正常结果应该返回一条记录:SKU 名称为“抿节基础套餐(含15种小料)”,价格是 1600,配料数量是 15。到这里,菜单部分已经具备可扩展性。

5. 核心代码:下单、支付与续面校验

5.1 封装数据库连接

noodle_shop.py中先写数据库连接函数:

import sqlite3 import uuid from datetime import datetime, timedelta DB_FILE = "noodle_shop.db" def connect(db_file=DB_FILE): conn = sqlite3.connect(db_file) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") return conn def generate_order_no(): timestamp = datetime.now().strftime("%Y%m%d%H%M%S") return f"{timestamp}-{uuid.uuid4().hex[:6].upper()}"

数据库连接时开启PRAGMA foreign_keys = ON非常关键。SQLite 默认不启用外键约束,不开启的话,即使存在无效的sku_id,插入订单明细也不会报错。开启后,数据完整性会更有保障。

5.2 下单:校验 SK

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 10:44:18

Spring Boot国际化实战:从零构建法语(fr-FR)本地化支持

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 10:44:10

薛之谦2023天外来物石家庄演唱会背后的技术支撑

这个标题对应的是一台演唱会主题活动&#xff0c;而不是可安装、可部署或可接口调用的技术项目。目前输入里没有项目正文、没有关键词、没有摘要说明&#xff0c;也没有相关技术细节可以用作事实依据&#xff0c;所以我无法在不虚构内容的前提下&#xff0c;把这件事包装成一篇…

作者头像 李华
网站建设 2026/9/3 10:41:46

人形机器人淘汰赛:从演示到可靠运行的工程化攻坚

开篇不需要破题仪式。先给一个判断&#xff1a;人形机器人行业真正开始“去泡沫”&#xff0c;不是从某场发布会开始的&#xff0c;而是从越来越多团队发现“做一个惊艳的两分钟演示人人都会&#xff0c;但让机器人在产线旁连续运行、稳定产出、停下来不惹麻烦”这种工作极其艰…

作者头像 李华
网站建设 2026/9/3 10:40:43

k6 性能测试快速指南:从安装到读懂结果只要 4 步

k6 性能测试快速指南&#xff1a;从安装到读懂结果只要 4 步 【免费下载链接】k6 A modern load testing tool, using Go and JavaScript 项目地址: https://gitcode.com/GitHub_Trending/k6/k6 如果你是被要求"验证一下系统能扛多少量"却还没工具下手的开发或…

作者头像 李华
网站建设 2026/9/3 10:40:38

MAS 激活脚本:3 步免费激活 Windows 和 Office 的完整新手指南

MAS 激活脚本&#xff1a;3 步免费激活 Windows 和 Office 的完整新手指南 【免费下载链接】Microsoft-Activation-Scripts Open-source Windows and Office activator featuring HWID, Ohook, TSforge, and Online KMS activation methods, along with advanced troubleshooti…

作者头像 李华
网站建设 2026/9/3 10:40:13

RTDumperGUIv19:嵌入式固件提取工具的原理、实战与生态解析

简介&#xff1a;RTDumperGUIv19是一款面向网络安全分析人员、数字取证初学者及Windows系统运维工程师的轻量级内存取证辅助工具&#xff0c;专为64位Windows环境优化&#xff0c;解决内存dump操作门槛高、命令行工具不易调试等实际问题。压缩包共4个文件&#xff08;59KB&…

作者头像 李华