⚠️ 免责声明:本文项目仅供技术学习参考,严禁直接作为毕业设计或课程作业提交。请结合自身需求进行二次开发与创新,学术诚信从我做起 🙂
先把丑话说前头
一个电商系统好不好用,大半要看数据库设计得合不合理。
表结构设计对了,后面写代码一路顺畅;设计错了,越往后越难受——加字段、改关系、拆表,改到怀疑人生。
这篇把 MYD 电商系统的完整数据库设计掏出来:ER 图、12张核心表、每张表的字段设计思路、完整建表 SQL。
照着建,你的毕设数据库这一关直接过。
整体数据关系
先上一张 ER 关系图,心里有个全貌:

核心关系很清晰:
- 用户是中心,围着用户的有购物车、订单、地址、收藏
- 商品是另一个中心,围着商品的有分类、图片、购物车项、订单项
- 订单和订单项是一对多,一个订单包含多个商品
12张表逐张拆解
1. 用户表(user)
用户是整个系统的核心,存基本信息就行,别什么都往里塞。
| id | INT | 用户ID | 主键,自增 |
| username | VARCHAR(50) | 用户名 | 唯一,用于登录 |
| password | VARCHAR(255) | 密码 | 加密存储,不明文 |
| VARCHAR(100) | 邮箱 | 可用于找回密码 | |
| phone | VARCHAR(20) | 手机号 | 可选 |
| nickname | VARCHAR(50) | 昵称 | 页面显示用 |
| avatar | VARCHAR(255) | 头像URL | 可选,有默认头像 |
| status | TINYINT | 账号状态 | 0=禁用 1=正常 |
| created_at | DATETIME | 注册时间 | |
| updated_at | DATETIME | 更新时间 |
设计思路:
- 密码字段长度一定要给够(255),因为加密后字符串很长
- status 字段很重要,毕设答辩老师大概率会问"怎么封禁用户"
- 时间字段每个表都要有,养成好习惯
2. 管理员表(admin)
后台管理员单独建表,不跟用户表混在一起——权限体系不一样。
| id | INT | 管理员ID | 主键,自增 |
| username | VARCHAR(50) | 账号 | 唯一 |
| password | VARCHAR(255) | 密码 | 加密存储 |
| role | VARCHAR(20) | 角色 | super/editor 等 |
| last_login | DATETIME | 最后登录时间 | |
| created_at | DATETIME | 创建时间 |
3. 商品分类表(category)
分类是树形结构,但毕设不用搞那么复杂——一级分类就够用了。
| id | INT | 分类ID | 主键,自增 |
| name | VARCHAR(50) | 分类名称 | 如"手机数码"“服装鞋帽” |
| sort | INT | 排序权重 | 数字越小越靠前 |
| status | TINYINT | 状态 | 0=隐藏 1=显示 |
| created_at | DATETIME | 创建时间 |
4. 商品表(goods)
这是最重要的一张表,字段也最多。
| id | INT | 商品ID | 主键,自增 |
| category_id | INT | 分类ID | 外键关联category |
| name | VARCHAR(200) | 商品名称 | |
| description | TEXT | 商品描述 | 富文本详情 |
| price | DECIMAL(10,2) | 售价 | 用DECIMAL不用FLOAT |
| original_price | DECIMAL(10,2) | 原价 | 划线价用 |
| stock | INT | 库存数量 | |
| sales | INT | 销量 | 初始为0 |
| cover_image | VARCHAR(255) | 封面图 | 列表页展示用 |
| status | TINYINT | 上架状态 | 0=下架 1=上架 |
| is_hot | TINYINT | 是否热销 | 0=否 1=是,首页推荐用 |
| is_new | TINYINT | 是否新品 | 0=否 1=是 |
| sort | INT | 排序权重 | |
| created_at | DATETIME | 创建时间 | |
| updated_at | DATETIME | 更新时间 |
设计思路:
- 价格必须用 DECIMAL,绝对不能用 FLOAT/DOUBLE——浮点数精度问题会导致金额计算出错,毕设答辩踩这个雷就凉了
- 销量字段冗余存一份,不用每次都去订单表里统计,性能好很多
- 封面图单独存字段,列表页不用去关联图片表,查一次就够
5. 商品图片表(goods_image)
一个商品多张图,单独建表。
| id | INT | 图片ID | 主键,自增 |
| goods_id | INT | 商品ID | 外键 |
| image_url | VARCHAR(255) | 图片地址 | |
| sort | INT | 排序 | 第一张默认主图 |
| created_at | DATETIME | 创建时间 |
6. 购物车表(cart)
购物车很简单——哪个用户、加了哪个商品、几件。
| id | INT | 购物车项ID | 主键,自增 |
| user_id | INT | 用户ID | 外键 |
| goods_id | INT | 商品ID | 外键 |
| quantity | INT | 数量 | 最少1件 |
| selected | TINYINT | 是否选中 | 结算时用 |
| created_at | DATETIME | 添加时间 | |
| updated_at | DATETIME | 更新时间 |
设计思路:
- selected 字段很实用——购物车里可以选部分商品结算,不是全选才能结
- user_id + goods_id 加唯一索引,同一个商品不重复加,只更新数量
7. 收货地址表(address)
一个用户多个地址,有一个默认地址。
| id | INT | 地址ID | 主键,自增 |
| user_id | INT | 用户ID | 外键 |
| receiver | VARCHAR(50) | 收货人姓名 | |
| phone | VARCHAR(20) | 手机号 | |
| province | VARCHAR(50) | 省 | |
| city | VARCHAR(50) | 市 | |
| district | VARCHAR(50) | 区/县 | |
| detail | VARCHAR(255) | 详细地址 | |
| is_default | TINYINT | 是否默认 | 0=否 1=是 |
| created_at | DATETIME | 创建时间 |
8. 订单表(order_info)
订单主表,注意表名别叫 order——这是 SQL 关键字,会踩坑。
| id | INT | 订单ID | 主键,自增 |
| order_no | VARCHAR(32) | 订单号 | 唯一,业务用这个 |
| user_id | INT | 用户ID | 外键 |
| address_id | INT | 收货地址ID | 快照存一份也行 |
| total_amount | DECIMAL(10,2) | 订单总金额 | |
| pay_amount | DECIMAL(10,2) | 实付金额 | 优惠后金额 |
| status | TINYINT | 订单状态 | 见下方说明 |
| pay_type | VARCHAR(20) | 支付方式 | alipay/wechat 等 |
| pay_time | DATETIME | 支付时间 | |
| ship_time | DATETIME | 发货时间 | |
| finish_time | DATETIME | 完成时间 | |
| cancel_time | DATETIME | 取消时间 | |
| remark | VARCHAR(500) | 订单备注 | 用户留言 |
| created_at | DATETIME | 创建时间 |
在 Navicat 里建好后,order_info 的字段设计界面长这样——字段名、类型、长度、主键、注释一目了然:

订单状态设计(必考题):
| 0 | 待支付 | 下单后未付款 |
| 1 | 待发货 | 已付款未发货 |
| 2 | 待收货 | 已发货未确认 |
| 3 | 已完成 | 确认收货 |
| 4 | 已取消 | 用户取消 |
| 5 | 退款中 | 申请退款 |
| 6 | 已退款 | 退款完成 |
设计思路:
- 表名用 order_info 或者 orders,别叫 order,SQL 里会报错
- 订单号是业务主键,不用自增 ID——订单号要"看不出规律+不重复+有长度",一般用时间戳+随机数生成
- 地址这里可以存 ID 关联,也可以把地址信息快照存进来(推荐快照方式,因为用户可能后来改了地址,历史订单的地址不能变)
- 状态流转是答辩常问的点:待支付 → 待发货 → 待收货 → 已完成,中间可以走取消和退款
9. 订单明细表(order_item)
一个订单多个商品,单独拆明细表。
| id | INT | 明细ID | 主键,自增 |
| order_id | INT | 订单ID | 外键 |
| goods_id | INT | 商品ID | 外键 |
| goods_name | VARCHAR(200) | 商品名称快照 | 下单时的名字 |
| goods_image | VARCHAR(255) | 商品图片快照 | |
| price | DECIMAL(10,2) | 下单时单价 | |
| quantity | INT | 购买数量 | |
| subtotal | DECIMAL(10,2) | 小计金额 | price × quantity |
| created_at | DATETIME | 创建时间 |
设计思路:
- 商品名称、图片、价格都做快照存下来——因为商品以后可能改名、改价,但历史订单得保留下单时的样子
- subtotal 冗余存,不用每次算
10. 收藏表(favorite)
| id | INT | 收藏ID | 主键,自增 |
| user_id | INT | 用户ID | 外键 |
| goods_id | INT | 商品ID | 外键 |
| created_at | DATETIME | 收藏时间 |
user_id + goods_id 加唯一索引,一个商品只能收藏一次。
11. 购物车表补充说明
购物车表前面列过了,这里补一个注意点:
加购时的库存校验——别等下单才检查库存,加入购物车的时候也可以检查一下,提示用户"库存不足"。
但加购时不用扣库存,下单的时候才扣——不然用户加了不买,库存就被占死了。
12. 系统设置表(system_config)
存网站配置,后台可以改。
| id | INT | ID | 主键,自增 |
| config_key | VARCHAR(50) | 配置键 | 如 site_name |
| config_value | TEXT | 配置值 | |
| description | VARCHAR(255) | 说明 | 备注用 |
键值对结构,灵活——想加什么配置直接往里插数据就行,不用改表结构。
完整建表 SQL(MySQL版)
把下面这段 SQL 直接复制到 Navicat 或 phpMyAdmin 里执行,12张表一次性建好。
— 创建数据库
CREATE DATABASE IF NOT EXISTS myd_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE myd_shop;
— 1. 用户表
CREATE TABLE `user` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`password` VARCHAR(255) NOT NULL COMMENT '密码(加密存储)',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
`status` TINYINT DEFAULT 1 COMMENT '状态 0=禁用 1=正常',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
— 2. 管理员表
CREATE TABLE `admin` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '管理员ID',
`username` VARCHAR(50) NOT NULL COMMENT '账号',
`password` VARCHAR(255) NOT NULL COMMENT '密码',
`role` VARCHAR(20) DEFAULT 'editor' COMMENT '角色',
`last_login` DATETIME DEFAULT NULL COMMENT '最后登录时间',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表';
— 3. 商品分类表
CREATE TABLE `category` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` VARCHAR(50) NOT NULL COMMENT '分类名称',
`sort` INT DEFAULT 0 COMMENT '排序权重',
`status` TINYINT DEFAULT 1 COMMENT '状态 0=隐藏 1=显示',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表';
— 4. 商品表
CREATE TABLE `goods` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`category_id` INT NOT NULL COMMENT '分类ID',
`name` VARCHAR(200) NOT NULL COMMENT '商品名称',
`description` TEXT COMMENT '商品描述',
`price` DECIMAL(10,2) NOT NULL COMMENT '售价',
`original_price` DECIMAL(10,2) DEFAULT NULL COMMENT '原价',
`stock` INT DEFAULT 0 COMMENT '库存',
`sales` INT DEFAULT 0 COMMENT '销量',
`cover_image` VARCHAR(255) DEFAULT NULL COMMENT '封面图',
`status` TINYINT DEFAULT 1 COMMENT '上架状态 0=下架 1=上架',
`is_hot` TINYINT DEFAULT 0 COMMENT '是否热销',
`is_new` TINYINT DEFAULT 0 COMMENT '是否新品',
`sort` INT DEFAULT 0 COMMENT '排序权重',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
— 5. 商品图片表
CREATE TABLE `goods_image` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '图片ID',
`goods_id` INT NOT NULL COMMENT '商品ID',
`image_url` VARCHAR(255) NOT NULL COMMENT '图片地址',
`sort` INT DEFAULT 0 COMMENT '排序',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_goods_id` (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品图片表';
— 6. 购物车表
CREATE TABLE `cart` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '购物车项ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`goods_id` INT NOT NULL COMMENT '商品ID',
`quantity` INT DEFAULT 1 COMMENT '数量',
`selected` TINYINT DEFAULT 1 COMMENT '是否选中 0=否 1=是',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_goods` (`user_id`, `goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';
— 7. 收货地址表
CREATE TABLE `address` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '地址ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`receiver` VARCHAR(50) NOT NULL COMMENT '收货人',
`phone` VARCHAR(20) NOT NULL COMMENT '手机号',
`province` VARCHAR(50) NOT NULL COMMENT '省',
`city` VARCHAR(50) NOT NULL COMMENT '市',
`district` VARCHAR(50) NOT NULL COMMENT '区/县',
`detail` VARCHAR(255) NOT NULL COMMENT '详细地址',
`is_default` TINYINT DEFAULT 0 COMMENT '是否默认 0=否 1=是',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货地址表';
— 8. 订单表(注意:表名不叫order,因为order是SQL关键字)
CREATE TABLE `order_info` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
`user_id` INT NOT NULL COMMENT '用户ID',
`receiver_name` VARCHAR(50) NOT NULL COMMENT '收货人(快照)',
`receiver_phone` VARCHAR(20) NOT NULL COMMENT '收货电话(快照)',
`receiver_address` VARCHAR(500) NOT NULL COMMENT '收货地址(快照)',
`total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
`pay_amount` DECIMAL(10,2) NOT NULL COMMENT '实付金额',
`status` TINYINT DEFAULT 0 COMMENT '订单状态 0待支付 1待发货 2待收货 3已完成 4已取消 5退款中 6已退款',
`pay_type` VARCHAR(20) DEFAULT NULL COMMENT '支付方式',
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
`ship_time` DATETIME DEFAULT NULL COMMENT '发货时间',
`finish_time` DATETIME DEFAULT NULL COMMENT '完成时间',
`cancel_time` DATETIME DEFAULT NULL COMMENT '取消时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '订单备注',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
— 9. 订单明细表
CREATE TABLE `order_item` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '明细ID',
`order_id` INT NOT NULL COMMENT '订单ID',
`goods_id` INT NOT NULL COMMENT '商品ID',
`goods_name` VARCHAR(200) NOT NULL COMMENT '商品名称(快照)',
`goods_image` VARCHAR(255) DEFAULT NULL COMMENT '商品图片(快照)',
`price` DECIMAL(10,2) NOT NULL COMMENT '下单时单价',
`quantity` INT NOT NULL COMMENT '购买数量',
`subtotal` DECIMAL(10,2) NOT NULL COMMENT '小计金额',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_goods_id` (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';
— 10. 收藏表
CREATE TABLE `favorite` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '收藏ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`goods_id` INT NOT NULL COMMENT '商品ID',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_goods` (`user_id`, `goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';
— 11. 系统配置表
CREATE TABLE `system_config` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'ID',
`config_key` VARCHAR(50) NOT NULL COMMENT '配置键',
`config_value` TEXT COMMENT '配置值',
`description` VARCHAR(255) DEFAULT NULL COMMENT '说明',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
— 12. 轮播图表(可选,首页轮播用)
CREATE TABLE `banner` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT 'ID',
`title` VARCHAR(100) DEFAULT NULL COMMENT '标题',
`image_url` VARCHAR(255) NOT NULL COMMENT '图片地址',
`link_url` VARCHAR(255) DEFAULT NULL COMMENT '跳转链接',
`sort` INT DEFAULT 0 COMMENT '排序',
`status` TINYINT DEFAULT 1 COMMENT '状态 0=禁用 1=启用',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轮播图表';
把上面这段 SQL 在 Navicat 里跑完,左侧就能看到建好的 12 张表:

数据库这块,答辩老师会追着问的
为什么订单地址要做快照,不直接关联地址表?
答:因为用户可能修改或删除地址,如果订单直接关联地址ID,历史订单的收货信息就会丢失甚至查不到。快照方式保证了订单数据的完整性和可追溯性。
为什么价格字段用 DECIMAL 不用 FLOAT?
答:FLOAT 是浮点数,存在精度丢失问题,金额计算可能出现 0.1+0.2=0.30000000000000004 这种情况。DECIMAL 是定点数,精确计算,金融相关字段必须用 DECIMAL。
订单表为什么叫 order_info 不叫 order?
答:因为 ORDER 是 SQL 关键字(ORDER BY),用关键字做表名会导致 SQL 语句报错或需要加反引号,增加出错概率。
购物车为什么不存价格?
答:购物车里的价格是动态的,以商品表当前价格为准——因为商品可能调价。下单时才取当时的价格快照存到订单项里。
怎么防止超卖?
答:用数据库事务 + 乐观锁/悲观锁。下单时在事务内先查库存,再扣减,条件更新(UPDATE goods SET stock = stock – ? WHERE id = ? AND stock >= ?),影响行数为0说明库存不足,事务回滚。
数据库设计这事,我第一版就吃过亏:订单表直接叫 order,MySQL 报错,查了半天才反应过来是关键字。
表这玩意儿,说白了就是把业务里那点关系画成格子。业务想清楚了,表结构自然就出来了。
上面这12张表,我这个小商城全部功能都够用了;以后想加优惠券、秒杀、拼团、评价系统,也都是加张表的事。
梅雅达编程笔记原创 · 首发 CSDN · 转载请注明出处
网硕互联帮助中心




评论前必须登录!
注册