尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
外卖系统数据库设计:22张表拆解订单链路与SQL避坑指南
简介这份PDF文档围绕外卖系统数据库设计展开面向小程序外卖开发与数据库学习者。文档以美团、饿了么为参考完整给出用户表、用户地址表、店铺表、商家登录及店铺信息表的建表SQL涵盖字段类型、主键、索引及外键约束等设计要点并梳理了第三范式、数据冗余最小化、密码加密、查询性能优化等实践知识。同时文档对每个字段都附有中文注释如用户名采用VARCHAR(50)、钱包使用DECIMAL、状态用TINYINT等便于理解字段含义与数据类型选择。包体为单个PDF文件大小仅148KB方便直接下载阅读。已有207人学习下载。读者可从中获得一套可直接参考的外卖库表结构设计理解用户、地址、商家之间的数据关系以及如何在程序中实现外键约束可为课程设计、项目原型或数据库入门提供实用参考。1. 外卖系统数据库设计全套 SQL字段、权限与订单链路的取舍这份 SQL 脚本我拆完后最大的感受是它把「外卖业务里最脏最累的部分」都提前考虑到了。22 张表覆盖用户、商家、菜品、订单、支付、退款、红包、评论、通知几乎是把美团和饿了么的核心交易链路按自己的理解重做了一遍。最反直觉的一点是全库没有一条外键约束设计者明确说外键约束放在程序层控制。这意味着查询性能更好、写入更灵活但也在提醒你——这库的设计逻辑必须在业务代码里用事务和校验补回来。适合谁做小程序外卖课程设计的学生、想快速搭建外卖后台原型的新手、以及准备做电商订单域设计的后端开发。花半小时把这个脚本读透能少走很多弯路。下面我按用户端、商家端、订单链路、营销互动四个维度拆开讲最后给你一份避坑清单。2. 用户与商家两侧的表设计从 user 到 shop_info 的字段取舍2.1 用户侧user 表与 user_address 表的冗余与默认值逻辑先看user表它是整个系统的登录主表。字段包括mobile、password、open_id、wallet、status、add_time外卖小程序最常见的登录组合是「手机号验证码 微信授权」所以把mobile和open_id同时放进来是合理的。CREATE TABLE IF NOT EXISTS user( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR (50) COMMENT 用户昵称, mobile VARCHAR (20) COMMENT 联系电话, password VARCHAR (50) COMMENT 登录密码, open_id VARCHAR (100) COMMENT 微信openid, wallet DECIMAL DEFAULT 0 COMMENT 钱包, email VARCHAR (50) COMMENT 邮箱, truename VARCHAR (50) COMMENT 用户真实姓名, gender VARCHAR (10) COMMENT 性别, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 用户登录表;这里有两个值得注意的设计习惯。第一status字段默认值是10而不是1或0这是很多实际项目里的惯例不用 0 和 1 表示状态因为后续扩展状态机时比如 10 正常、20 禁用、30 注销代码里加 new status 不会跟旧值语义冲突。第二add_time用的是INT(11)存 Unix 时间戳而不是DATETIME配合date(Y-m-d H:i:s, $row[add_time])转成可读时间。这种存法省空间、排序方便但代价是写 SQL 查询时得时刻记得转格式不能在可视化工具里直接看时间。接下来是user_address配送地址表。user_id关联user表字段把省、市、区、街道、详细地址拆得很细另外还有longitude和latitude存经纬度用于后续的配送距离计算和商家排序。注意gender字段默认值是字符串先生这说明在设计者预期的场景里下单人大概率就是收货人默认填先生省事但实际做产品时会用性别单选按钮先生/女士这种文案值直接写进表里建议后续改成枚举值或 TINYINT 数字。CREATE TABLE IF NOT EXISTS user_address( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, user_id INT(11) NOT NULL DEFAULT 0 COMMENT 用户ID, username VARCHAR (50) COMMENT 姓名, gender VARCHAR(10) DEFAULT 先生 COMMENT 性别, mobile VARCHAR (20) COMMENT 联系电话, province VARCHAR (50) COMMENT 省, city VARCHAR (50) COMMENT 市, district VARCHAR (50) COMMENT 区, longitude VARCHAR (20) COMMENT 经度, latitude VARCHAR (20) COMMENT 纬度, address VARCHAR (200) COMMENT 详细地址, street VARCHAR (100) COMMENT 街道,门牌号, tag TINYINT DEFAULT 0 COMMENT 标签, default TINYINT DEFAULT 0 COMMENT 是否为默认地址, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间, edit_time INT(11) DEFAULT 0 COMMENT 编辑时间 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 用户配送地址;default是 MySQL 的保留字虽然加了反引号能用但建议改成is_default否则在框架的查询构造器里容易踩坑。从设计角度看user_address把「地址」当值对象冗余存储在独立表里是第三范式允许的做法。一个用户对应多个地址user_id不设外键但加索引查询时WHERE user_id ? AND status 10取列表WHERE user_id ? AND default 1取默认地址性能可接受。2.2 商家侧shop、shop_info、shop_license 三表分工与入驻流程商家侧拆成了三张表shop是登录账号shop_info是商铺资料与配送参数shop_license是证照信息。这种拆法对应的是入驻流程先注册账号再完善资料最后提交资质审核。CREATE TABLE IF NOT EXISTS shop( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, shopname VARCHAR (50) COMMENT 商品名称, mobile VARCHAR (20) COMMENT 联系电话, password VARCHAR (50) COMMENT 密码, email VARCHAR (50) COMMENT 邮箱, login_info VARCHAR (500) COMMENT 登录信息, num_login_error TINYINT DEFAULT 0 COMMENT 登录错误次数, time_login_lock INT (11) DEFAULT 0 COMMENT 锁定登录时间, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间 )ENGINEInnoDB DEFAULT CHARSETutf8 AUTO_INCREMENT10000 COMMENT 商家登录;注意AUTO_INCREMENT10000这个细节说明设计者希望商家 ID 从 10000 开始常见做法是让用户表和商家表的 ID 区分开联调时一眼能看出数据来源于哪个表。num_login_error和time_login_lock是密码错误锁定机制登录失败一次加一超过阈值后在time_login_lock存的时间戳之前不允许登录。这个字段在真实项目里很常见但要注意在登录成功时清零。shop_info是核心资料表字段特别多包括营业时间、坐标、门店图片、配送参数等。其中begin_time和end_time用的是INT(11)存时间戳但营业时间这种字段其实更应该用VARCHAR(5)存 09:00 这种格式或者用 TIME 类型。用时间戳存营业时间查询时要做多次 FROM_UNIXTIME 转换比较麻烦。CREATE TABLE IF NOT EXISTS shop_info( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, shop_id INT(11) DEFAULT 0 COMMENT 商店ID, tag VARCHAR (100) COMMENT 商铺所属的TAG, shopname VARCHAR (50) COMMENT 商品名称, contact_man VARCHAR (20) COMMENT 联系人, mobile VARCHAR (20) COMMENT 外卖电话, cateid INT (11) DEFAULT 0 COMMENT 门店类型, longitude VARCHAR (20) COMMENT 经度, latitude VARCHAR (20) COMMENT 纬度, province VARCHAR (20) COMMENT 省, city VARCHAR (20) COMMENT 市, district VARCHAR (20) COMMENT 区, address VARCHAR (200) COMMENT 详细地址, street VARCHAR (100) COMMENT 街道/门牌号, score FLOAT DEFAULT 0 COMMENT 平均评分, send_time VARCHAR (50) COMMENT 配送时间, box_cost DECIMAL DEFAULT 0 COMMENT 餐盒费用, send_cost DECIMAL DEFAULT 0 COMMENT 配送费用, floor_send_cost DECIMAL DEFAULT 0 COMMENT 起送消费 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 商铺信息表;配送三件套box_cost、send_cost、floor_send_cost放在了shop_info里这个位置合理因为它们是商铺级配置不是订单级。但注意这三个字段都没指定 DECIMAL 精度DECIMAL 默认是(10,0)也就是说只能存整数如果餐盒费是 1.5 元就会存成 2。后面我把它列进避坑清单这里先埋个伏笔。shop_license把身份证和营业执照拆成一堆字段idcard_img、business_img、license_img都是 VARCHAR(500) 存图片 URL。业务上商家入驻需要提交资质这里把三种图片分别存了三个字段实际做审核流时一般会拆成一张审核材料表和一张审核记录表而不是把每个图片路径都列成独立字段。不过对于课程设计这个表够用了审核时UPDATE shop_license SET status 20 WHERE shop_id ?就行。3. 菜品与活动的库表实现food、food_category、shop_activity 怎么配合3.1 food 表字段解读折扣、限购、销量与规格选项food表是菜单表关联shop_id和cate_id。关键字段有origin_price、sell_price、discount、limit_num、option、total_sales、month_sales、praise_rate。origin_price是原价sell_price是实际售价discount是折扣比例默认 10不打折。CREATE TABLE IF NOT EXISTS food( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, shop_id INT(11) NOT NULL DEFAULT 0 COMMENT 商店ID, cate_id INT(11) DEFAULT 0 COMMENT 分类ID, title VARCHAR (50) COMMENT 食品名字, desc VARCHAR (100) COMMENT 描述, cover VARCHAR (500) COMMENT 食品封面图, origin_price DECIMAL DEFAULT 0 COMMENT 原价, sell_price DECIMAL DEFAULT 0 COMMENT 售价, discount DECIMAL DEFAULT 10 COMMENT 折扣, like INT (11) DEFAULT 0 COMMENT 点赞, limit_num INT (11) DEFAULT 0 COMMENT 限购数量, option VARCHAR (500) COMMENT 规格选项, total_sales INT (11) COMMENT 总的销量, month_sales INT (11) COMMENT 月销量, praise_rate FLOAT DEFAULT 100 COMMENT 好评率, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 菜品信息表;这里值得展开讲的是option字段VARCHAR(500) 存规格选项。外卖场景里「辣度微辣/中辣/特辣」和「加料鸡蛋/培根/芝士」这类规格常见做法是用 JSON 存比如[{name:辣度,values:[微辣,中辣,特辣]}]。查询菜品详情时直接把这个字段传给前端解析用不着 JOIN 一张规格表。如果你的框架支持 JSON 字段类型MySQL 5.7 可以把这个字段改成 JSON 类型查询时用JSON_EXTRACT处理。这个设计适合中小型项目但注意后端要校验 JSON 格式不能让前端传了脏数据直接入库。month_sales和total_sales是典型的反范式冗余字段。真实项目里销量通常是从订单表GROUP BY food_id实时统计的但外卖 App 的列表页要高频展示「月售 1000」每次都去订单表聚合会把数据库打爆。所以常见的做法是每晚定时任务跑一次统计然后更新到food表里。但要注意like这个字段名在 MySQL 里虽然能当列名但在SELECT * WHERE like 1这种写法里容易和 LIKE 运算符混淆建议改成like_num。food_category是商家的菜品分类表字段比较简单shop_id、name、desc、status。一个商家对应多个分类一个分类下多个菜品。CREATE TABLE IF NOT EXISTS food_category( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, shop_id INT (11) DEFAULT 0 COMMENT 商铺ID, name VARCHAR (50) COMMENT 分类类型, desc VARCHAR (500) COMMENT 描述, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 商家的食物分类;查询某商家所有菜品时先查food_category拿分类列表再按cate_id查food表或者直接用一条 SQL 配合ORDER BY cate_id, sort排序。这个表不复杂但status字段的用法值得注意分类下架时把status改成非 10 的值而不是删记录这样历史订单里的分类 ID 仍然能追溯到名字不会因为删除而断链。3.2 shop_activity 满减活动target 与 cut 的配对以及和 coupon 的区别shop_activity是商家活动表字段有type、shop_id、target、cut。target是满减门槛cut是优惠金额。比如「满 30 减 5」target30、cut5。CREATE TABLE IF NOT EXISTS shop_activity ( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, type TINYINT DEFAULT 0 COMMENT 活动分类, shop_id INT (11) DEFAULT 0 COMMENT 商铺ID, target DECIMAL DEFAULT 0 COMMENT 满足的消费金额, cut DECIMAL DEFAULT 0 COMMENT 优惠金额, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 商家活动;type字段定义活动类型比如 1 表示满减、2 表示折扣、3 表示新客立减具体枚举可以在mysite配置表里维护。实际使用时下单前接口要把商家的所有活动查出来按target从高到低排序然后计算最优惠的组合。设计上有个小问题shop_activity和coupon表功能上有重叠coupon是平台或商家发的红包shop_activity是商家自己做的满减活动。满减是只要消费达到门槛就自动减红包是要用户领取后才能用这是两种不同的营销模型分开设计是对的。再看mysite表这是网站基本配置表也叫 KV 表。type加key做了联合唯一索引un_keyvalue字段用 TEXT。CREATE TABLE IF NOT EXISTS mysite( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, type TINYINT DEFAULT 0 COMMENT 分类, key VARCHAR (100) COMMENT 键, value text COMMENT 值, CONSTRAINT un_key UNIQUE (type,key) )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 网站基本设置;这种 KV 表非常适合存系统参数客服电话、App 版本号、分享文案、支付开关、活动开关等。type做命名空间key做键名value存值。如果你的框架有缓存组件可以把整表缓存到 Rediskey 用mysite:{type}:{key}读取时先查缓存再落库。注意key是 MySQL 保留字建表时虽然加了反引号但建议改成k或config_key。4. 订单域的表设计order、order_detail、order_food 三表链路4.1 订单金额拆分从总额到优惠再到实付order表是整个数据库设计的灵魂也是一份外卖订单从创建到完成的核心载体。它把金额拆成六个字段total_money商品总额、box_cost餐盒费、send_cost配送费、discount_money活动优惠金额、coupon_money红包优惠金额、pay_money实付金额。这六个字段的完整关系是pay_money total_money box_cost send_cost - discount_money - coupon_money。CREATE TABLE IF NOT EXISTS order( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, order_id VARCHAR (50) NOT NULL UNIQUE COMMENT 订单ID, user_id INT (11) DEFAULT 0 COMMENT 用户ID, shop_id INT (11) DEFAULT 0 COMMENT 商铺ID, box_cost DECIMAL DEFAULT 0 COMMENT 餐盒费, send_cost DECIMAL DEFAULT 0 COMMENT 配送费, total_money DECIMAL DEFAULT 0 COMMENT 总价, discount_money DECIMAL DEFAULT 0 COMMENT 优惠金额, coupon_id VARCHAR (50) COMMENT 红包ID, coupon_money DECIMAL DEFAULT 0 COMMENT 红包满减金额, pay_money DECIMAL DEFAULT 0 COMMENT 实付金额, pay_way TINYINT DEFAULT 0 COMMENT 支付方式, demand_time INT(11) DEFAULT 0 COMMENT 限定的时间, add_time INT (11) DEFAULT 0 COMMENT 加入时间, status TINYINT DEFAULT 1 COMMENT 状态 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 订单主表;这种金额拆分的设计值得学习。电商订单表最常见的错误是只存一个总金额后面做对账、退款、活动成本核算时全抓瞎。实际运营中「满 30 减 5 红包减 3 配送费 4 餐盒费 1 实付 27」这笔账要能对上就得把每一步优惠都单独存字段。另外注意order_id是业务订单号用VARCHAR(50)存UNIQUE约束保证唯一常见生成规则是时间戳 用户ID 随机数。demand_time是用户预约的送达时间。status字段默认值是1这和前面的status默认 10 不一致。如果你拿到这个脚本要改造成自己的项目建议统一状态码规范订单状态用1待支付、2已支付、3配送中、4已完成、5已取消、6退款中保持全库一致别让一部分表默认 10、一部分默认 1。4.2 订单快照与进度追踪order_detail 和 order_process 的用途order_detail字段很有想法它把下单那一刻的用户地址、联系方式和商家信息全部冗余了一份。为什么要这么做因为外卖场景里用户地址、商家营业信息都是可变的如果订单创建后用户改了地址、商家改了电话历史订单必须仍然显示下单时的信息。这就是快照思想。CREATE TABLE IF NOT EXISTS order_detail( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, order_id VARCHAR (50) NOT NULL UNIQUE COMMENT 订单ID, user_username VARCHAR (20) COMMENT 用户名, user_mobile VARCHAR (20) COMMENT 用户联系电话, user_address_id INT (11) DEFAULT 0 COMMENT 用户地址ID, user_address VARCHAR (500) COMMENT 用户详细地址, user_longitude VARCHAR (20) COMMENT 用户地址-经度, user_latitude VARCHAR (20) COMMENT 用户地址-纬度, shop_shopname VARCHAR (20) COMMENT 商铺名字, shop_mobile VARCHAR (20) COMMENT 商铺联系电话, shop_address VARCHAR (500) COMMENT 商铺详细地址, shop_longitude VARCHAR (20) COMMENT 商铺地址-经度, shop_latitude VARCHAR (20) COMMENT 商铺地址-纬度, deliver_id INT (11) COMMENT 送餐员ID, deliver_name VARCHAR (20) COMMENT 送餐员姓名, deliver_mobile VARCHAR (20) COMMENT 送餐员联系电话 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 订单详情表;这个表还预留了配送员字段deliver_id、deliver_name、deliver_mobile。外卖订单的三个核心参与方用户、商家、骑手都在这里留了快照订单详情页渲染时不用 JOIN 三张表直接查这一行就行。性能上很讨巧但当用户地址或商家信息更新时要小心新订单别用旧快照。实现方式是创建订单那一刻把数据原样复制进来。order_process是订单进度表字段有content、reason、order_status。它的作用是把订单状态流转的历史记录全部存下来用户下单、商家接单、骑手取餐、订单送达每一步写一条记录。这个表在设计上对应的是「订单追踪」功能用户在 App 上看到的物流时间线就是查这张表渲染出来的。CREATE TABLE IF NOT EXISTS order_process( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, order_id VARCHAR (50) COMMENT 订单ID, content VARCHAR (500) COMMENT 进度备注内容, reason VARCHAR (500) COMMENT 理由, order_status TINYINT DEFAULT 0 COMMENT 进度状态, status TINYINT DEFAULT 10 COMMENT 状态, add_time INT(11) DEFAULT 0 COMMENT 加入时间 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 订单--进度详情;实操时接单接口里要同时做两件事更新order表的status字段然后 INSERT 一条order_process记录。这两步要放在同一个事务里否则会出现状态已更新但进度没记录的问题。-- 示例商家接单操作更新订单状态并写入进度 START TRANSACTION; UPDATE order SET status 2 WHERE order_id 20250101120001 AND shop_id 10001; INSERT INTO order_process (order_id, content, order_status, status, add_time) VALUES (20250101120001, 商家已接单开始备餐, 2, 10, UNIX_TIMESTAMP()); COMMIT;这个事务有两个细节要注意。第一UPDATE 语句带了AND shop_id 10001这是防止商家 A 误操作商家 B 的订单程序里做权限校验时不能只靠前端隐藏按钮。第二UNIX_TIMESTAMP()可以直接生成当前时间戳存入add_time字段比先查时间再写入少一次查询。4.3 订单商品冗余order_food 为什么复制 food 表的标题和价格order_food表存订单里的每一个商品条目字段包括food_id、title、cover、origin_price、sell_price、number。注意它复制了food表的title、cover、origin_price、sell_price。CREATE TABLE IF NOT EXISTS order_food( id INT (11) PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 主键, order_id VARCHAR (50) COMMENT 订单ID, shop_id INT (11) DEFAULT 0 COMMENT 商铺ID, shopname VARCHAR (50) COMMENT 商铺名称, food_id INT (11) DEFAULT 0 COMMENT 商品ID, title VARCHAR (50) COMMENT 商品标题, cover VARCHAR (500) COMMENT 商品封面, origin_price DECIMAL DEFAULT 0 COMMENT 原价, sell_price DECIMAL DEFAULT 0 COMMENT 售价, number INT DEFAULT 0 COMMENT 下单数量 )ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT 订单商品详情表;前面说的快照思想在这里再次出现。商品改价、改图片后历史订单里的商品名称和价格必须保持原样否则用户查历史订单时看到的价格跟当时付的钱对不上容易产生纠纷。这也意味着下单接口里不能用「查询 food 表 JOIN」的方式渲染订单详情而应该直接用order_food里的冗余字段。order_id字段没有加 UNIQUE因为一个订单有多个商品一个订单 ID 对应多行记录索引类型建普通索引即可。5. 支付、退款、红包与评论的避坑指南6 个常见问题与排查方法5.1 金额精度坑DECIMAL 不写精度默认 (10,0)0.5 元的餐盒费直接变整数现象在 MySQL 里执行CREATE TABLE后插入1.5查询出来却显示2。原因DECIMAL默认精度是(10,0)也就是整数部分 10 位、小数部分 0 位。插入 1.5 时 MySQL 四舍五入成了 2。解决把所有金额字段显式改成DECIMAL(10,2)或DECIMAL(8,2)比如box_cost DECIMAL(10,2) DEFAULT 0。改完后1.5才能精确存储。-- 正确写法显式指定两位小数 ALTER TABLE shop_info MODIFY COLUMN box_cost DECIMAL(10,2) DEFAULT 0 COMMENT 餐盒费用, MODIFY COLUMN send_cost DECIMAL(10,2) DEFAULT 0 COMMENT 配送费用, MODIFY COLUMN floor_send_cost DECIMAL(10,2) DEFAULT 0 COMMENT 起送消费;同样的修改要应用到order表的所有金额字段、food表的origin_price、sell_price、discount以及coupon和shop_activity的target、cut。这是拿到这份 SQL 后第一件要做的事。5.2 保留字段名坑from、key、default等字段名需要反引号包裹现象执行建表语句时在notice表上提示 SQL 语法错误或者在框架里用查询构造器操作时报错。原因from、key、default、order都是 MySQL 的保留字或函数名。虽然原脚本里大多数都加了反引号但实际使用时不加反引号就会被 MySQL 解析器误读。解决建议把命名改掉from改成msg_fromkey改成config_keydefault改成is_default。如果不想改字段名所有 SQL 语句里必须加反引号。-- 推荐做法字段改名一劳永逸 ALTER TABLE notice CHANGE from msg_from VARCHAR(20) COMMENT 消息来源; ALTER TABLE mysite CHANGE key config_key VARCHAR(100) COMMENT 键; ALTER TABLE user_address CHANGE default is_default TINYINT DEFAULT 0 COMMENT 是否为默认地址;改完字段名后记得同步修改程序代码里的所有查询语句。我当时改order表名的时候WHERE 条件里写错一次就多调了半天血泪经验。5.3 订单号生成逻辑靠主键自增做订单号会翻车现象两个订单在同一秒创建时外部系统拿到的订单号重复或者在并发大的时候出现订单号位数不够。原因如果直接INSERT后取LAST_INSERT_ID()当订单号在并发环境下拿不到业务可用的全局唯一编号而且暴露了数据库自增规律。解决用程序生成业务订单号。常见做法是日期前缀加随机数补位PHP 侧生成后传入 SQL 插入。// 生成订单号年月日时分秒 微秒 随机数共 20 位以内 function generateOrderId($userId) { $datePart date(YmdHis); $microPart str_pad(floor(microtime(true) * 1000) % 1000, 3, 0, STR_PAD_LEFT); $randomPart str_pad(mt_rand(0, 9999), 4, 0, STR_PAD_LEFT); return $datePart . $microPart . $randomPart; } // 插入订单时显示传入 order_id $orderId generateOrderId($userId); $sql INSERT INTO order (order_id, user_id, shop_id, total_money, pay_money, status, add_time) VALUES ({$orderId}, {$userId}, {$shopId}, 30.00, 27.00, 1, . time() . );订单号生成后order表里靠UNIQUE约束保证唯一插入时如果违反唯一约束就重新生成一次。这个逻辑放在程序的服务层而不是数据库的触发器里方便后面接分布式 ID 方案时平滑替换。5.4 状态字段含义不一致user 表默认 10、order 表默认 1现象程序里判断订单状态时以为status 1是待支付结果拿到的数据列表里status 1的订单少了一半查半天才发现order_process表里订单状态用10表示已创建。原因这库是作者分模块写的每张表单独设计了状态值。用户表status10代表正常order表status1代表待支付food表status10代表上架。全库没有一个统一的状态字典表。解决在自己的系统里要么统一常量字典要么建一张sys_status表做配置。我在自己的项目里是在框架的配置文件里声明常量然后所有状态判断都用常量比对不写死数字。# 订单状态常量定义示例 ORDER_STATUS_PENDING_PAY 0 # 待支付 ORDER_STATUS_PAID 1 # 已支付 ORDER_STATUS_MAKING 2 # 备餐中 ORDER_STATUS_DELIVERING 3 # 配送中 ORDER_STATUS_FINISHED 4 # 已完成 ORDER_STATUS_CANCELED 5 # 已取消代码里写if order.status ORDER_STATUS_DELIVERING:就能避免魔法数字满天飞。否则后期加一个「退款中」状态时你不知道该用 6 还是 20。5.5 订单详情快照丢字段配送员信息在并发场景下写入失败现象订单详情页偶尔不显示骑手信息刷新后又出现了。原因订单创建时deliver_id、deliver_name、deliver_mobile是空值骑手接单后 UPDATEorder_detail更新配送员信息。如果 UPDATE 语句没带 WHERE 条件或者框架的乐观锁没生效高并发下会覆盖其他已经更新的字段。解决骑手接单的接口里UPDATE 语句必须带order_id且要判断当前配送员字段是否为空或者更新前先 SELECT 确认状态。-- 骑手接单只更新配送员信息且限定当前订单状态 UPDATE order_detail SET deliver_id 10086, deliver_name 张师傅, deliver_mobile 13800138000 WHERE order_id 20250101120001 AND deliver_id IS NULL;deliver_id IS NULL这个条件能防止两个骑手同时抢单时都更新成功。另外还可以在order表里加一个deliver_status字段用状态机控制可抢单 → 已接单 → 已取餐 → 已送达。5.6 评论回复树实现path 字段想清楚再写现象做评论的嵌套回复时查询「我回复的评论下的所有子评论」变得很困难用递归查询效率低。原因order_comment表里设计了path字段1/2/3/5和re_comment_id字段这是一个改进的前序遍历树结构。path存的是一条从根评论到当前评论的路径用斜杠分隔。但只存路径还不够查询某个节点下所有子评论需要WHERE path LIKE 1/2/%这个索引优化空间有限。解决如果评论量不大单条订单评论少于 100 条直接用re_comment_id做父子关联查出来后在程序里组树。如果数据量大给path字段加前缀索引或者用 MongoDB 之类的文档型数据库。外卖评论场景一般不会特别深两层就够用了。-- 给 path 字段加前缀索引优化 LIKE 查询 ALTER TABLE order_comment ADD INDEX idx_path (path(50));6. 把脚本跑起来导入步骤、验证 SQL 与三个可以扩展的字段拿到这份 SQL 后我一般会先在本机 MySQL 里完整跑一遍确认没有报错再根据自己项目的情况调整。下面是导入和验证的完整步骤。第一步创建数据库并导入脚本。假设你已经把 C 语言那段 SQL 存成了waimai.sql用命令行导入即可。mysql -u root -p waimai.sql导入完后检查是否报错然后连接数据库确认表数量。USE waimai; SHOW TABLES;如果一切正常应该能看到 22 张表。注意看表的数量如果少于这个数说明某张表建表时语法报错了需要回头检查第五列里的保留字和字段注释有没有格式问题。第二步验证核心表的字段完整性。重点看order表和order_detail表是否都建出来了这两个表是订单链路的核心。-- 查看订单表结构确认 DECIMAL 精度是否被默认成整数 DESC order;看到total_money这类字段的 Type 列是decimal(10,0)的话就按 5.1 小节的方法改成decimal(10,2)。所有金额字段都应该显示两位小数。第三步插入一条测试数据验证订单链路是不是通。多表联合插入建议拆成多条 SQL 逐条执行。-- 先用一条最简 SQL 验证基础表可用 INSERT INTO user (username, mobile, password, status, add_time) VALUES (测试用户, 13800000000, MD5(123456), 10, UNIX_TIMESTAMP());MD5(123456)只是演示实际项目里密码建议用password_hash()生成 bcrypt 哈希MD5 在安全上已经不够用了。插入成功后SELECT * FROM user WHERE mobile 13800000000能看到那条记录。第四步做一次最常见的查询演练查某商家的在售菜品列表并按分类分组。SELECT fc.name AS cate_name, f.title, f.sell_price FROM food f LEFT JOIN food_category fc ON f.cate_id fc.id WHERE f.shop_id 10001 AND f.status 10 ORDER BY fc.id, f.id;这条 SQL 里用LEFT JOIN连接分类表而不是子查询性能更优。f.status 10表示上架状态f.shop_id 10001是那家店的 ID。跑通这一步说明food和food_category的关联没问题。第五步模拟一次订单创建的完整事务。这是整个脚本里最值得验证的场景插入订单主表、插入订单详情、插入两个商品条目全在一个事务里。START TRANSACTION; -- 订单主表 INSERT INTO order (order_id, user_id, shop_id, box_cost, send_cost, total_money, discount_money, pay_money, status, add_time) VALUES (20250101120001, 1, 10001, 2.00, 4.00, 60.00, 5.00, 61.00, 1, UNIX_TIMESTAMP()); -- 订单详情快照 INSERT INTO order_detail (order_id, user_username, user_mobile, user_address, shop_shopname, shop_mobile, shop_address) VALUES (20250101120001, 测试用户, 13800000000, 北京市朝阳区xx路1号, 测试餐厅, 010-88888888, 北京市海淀区yy路2号); -- 订单商品 INSERT INTO order_food (order_id, shop_id, shopname, food_id, title, sell_price, number) VALUES (20250101120001, 10001, 测试餐厅, 1, 回锅肉, 30.00, 2); COMMIT;注意total_money 60.00是两份回锅肉的原价合计discount_money 5.00是满减比如满 60 减 5pay_money 60 2餐盒费 4配送费- 5 61。这个算术关系在代码里一定要保持一致否则后面对账全是坑。全部跑通后这套外卖系统数据库设计就可以放心拿去做二次开发了。如果要做课程设计答辩我建议把order表的金额拆分、order_detail的快照思想、shop_activity和coupon的双轨营销作为重点讲这三个点是整个设计里最能体现思考深度的部分——面试官最喜欢问的就是「为什么订单表要存这么多冗余字段」你能答出「快照 金额追溯 对账一致」这三点基本就过关了。从那以后我每次拿到项目数据库脚本都强制自己先跑一遍建表、再 INSERT 三条测试数据、再写一条跨表查询全流程走通了才敢往工程里引。这套 SQL 里的坑不算少但设计思路确实有值得抄的地方希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

佳能EDSDK C#开发实战:相机自动化控制从初始化到成片全流程

佳能EDSDK C#开发实战:相机自动化控制从初始化到成片全流程

简介:佳能EDSDK的C#完整开发示例,面向需要在.NET平台联机控制佳能相机的C#开发者,覆盖设备管理、实时预览、远程拍摄、图像下载与事件处理等核心场景。资源共18个文件,以8个C#源代码文件为主,包含主窗体实现、相机操作…

📅 2026/10/12 1:12:27
e2e不是缩写,而是工程协作的语境信号灯

e2e不是缩写,而是工程协作的语境信号灯

1. “e2e”不是缩写谜题,而是工程实践中最常被误读的信号灯最近在多个技术协作群里,频繁看到开发者发问:“这个需求里写的 e2e 是指什么?是端到端测试?还是端到端加密?抑或是边缘到边缘部署?”—…

📅 2026/10/12 1:12:27
CLRC663 Pin-to-Pin替代实战指南:硬件迁移的四大兼容维度与零PCB改动验证法

CLRC663 Pin-to-Pin替代实战指南:硬件迁移的四大兼容维度与零PCB改动验证法

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

📅 2026/10/12 1:07:27
MORE NEWS

更多资讯

📰

AnyPS5:低延迟本地串流方案,让任何设备秒变PS5延伸屏

1. 项目缘起与核心定位AnyPS5 这个名字第一次出现在我视野里的时候,我正蹲在一堆旧硬件中间翻找能用的零件。当时手头有一台闲置的迷你主机,配置不算差,但总觉得少了点什么——直到我看见这个标题。AnyPS5,拆开来看就是“Any”加“…

📰

中国土壤数据集从解压到栅格化的完整处理指南

简介:这是一份面向水文模型研究、农业规划、环境评估与城乡规划等场景的土壤专题数据包,旨在为科研与实践者提供全国尺度的土壤本底资料。内容覆盖土壤类型图、质地比例、容重、渗透率、含水量、pH、有机质及氮磷钾养分等关键参数,可用于径流…

📰

CH592蓝牙MCU选型与实战:低功耗无线外设开发指南

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

📰

Python数据预处理实战:从数据清洗到特征工程的全流程指南

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

📰

Xilem 内置 Emoji 名称数据集(emoji_names)解析:CSV 格式、数据溯源与 emoji_picker 示例实战

前端桌面应用 【免费下载链接】xilem An experimental Rust native UI framework 项目地址: https://gitcode.com/gh_mirrors/xil/xilem 点击查看 免费下载 本指南围绕 Xilem 仓库内 emoji_names 数据资源目录 展开,详细说明其 emoji.csv 数据的文件结构…

📰

WinForm中用ScottPlot绘制可拖拽贝塞尔曲线:坐标换算与实时刷新

简介:针对.NET平台的开源绘图库ScottPlot,提供了一份完整的WinForms图形展示演示包。该库以极为简洁的API实现折线图、柱状图、饼图、散点图以及贝塞尔曲线等大数据集交互式可视化,适合C#桌面应用开发者快速集成图表功能,也适用于…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬