MySQL: 综合项目:电商数据库设计与实现
最后更新:2026-08-26
Alice 创办的电商平台 ShopEasy 刚拿到融资,需要从 Excel 管理升级到 MySQL。她从零设计了完整的电商数据库,8 个模块覆盖从用户注册到数据分析的全流程。上线后日订单 5,000+ 笔,数据库稳如磐石。
1. 项目总览
- 涵盖用户、商品、订单、购物车、支付、统计、权限、备份 8 大模块
- 每个模块都有完整的建表 SQL + 业务 SQL
- 存储过程封装下单逻辑,事务保证数据一致性
- 视图实现数据统计,权限控制访问安全
- 备份脚本保障数据可恢复
(1) 模块与表清单
| 模块 | 核心表 | 主要功能 |
|---|---|---|
| 用户系统 | users / addresses / user_logs | 注册登录、地址管理、操作审计 |
| 商品系统 | categories / products / product_images | 无限级分类、商品CRUD、多图 |
| 订单系统 | orders / order_items | 下单、库存扣减、状态流转 |
| 购物车 | cart | 增删改查、合并购物车 |
| 支付记录 | payments | 支付状态、退款 |
| 数据统计 | 视图 | 热销商品、消费排行、月度报表 |
| 权限管理 | MySQL用户 | 只读/应用/管理员 |
| 备份策略 | mysqldump | 每日备份、保留策略 |
(2) 完整 ER 图
erDiagram
USERS ||--o{ ADDRESSES : has
USERS ||--o{ ORDERS : places
USERS ||--o{ CART : adds
USERS ||--o{ USER_LOGS : generates
CATEGORIES ||--o{ CATEGORIES : parent
CATEGORIES ||--o{ PRODUCTS : contains
PRODUCTS ||--o{ PRODUCT_IMAGES : has
PRODUCTS ||--o{ ORDER_ITEMS : included_in
PRODUCTS ||--o{ CART : added_to
ORDERS ||--|{ ORDER_ITEMS : contains
ORDERS ||--o| PAYMENTS : paid_by
USERS {
bigint id PK
varchar username
varchar email
varchar password_hash
varchar phone
enum status
}
ADDRESSES {
bigint id PK
bigint user_id FK
varchar receiver
varchar phone
varchar province
varchar city
varchar detail
tinyint is_default
}
USER_LOGS {
bigint id PK
bigint user_id FK
varchar action
varchar ip_address
datetime created_at
}
CATEGORIES {
bigint id PK
varchar name
bigint parent_id FK
int sort_order
}
PRODUCTS {
bigint id PK
varchar name
decimal price
int stock
bigint category_id FK
enum status
}
PRODUCT_IMAGES {
bigint id PK
bigint product_id FK
varchar image_url
tinyint is_main
int sort_order
}
ORDERS {
bigint id PK
varchar order_no
bigint user_id FK
decimal total_amount
enum status
}
ORDER_ITEMS {
bigint id PK
bigint order_id FK
bigint product_id FK
int quantity
decimal price
}
CART {
bigint id PK
bigint user_id FK
bigint product_id FK
int quantity
}
PAYMENTS {
bigint id PK
bigint order_id FK
varchar payment_no
decimal amount
enum method
enum status
}
2. 需求分析与 ER 设计
(1) 需求分析
电商系统核心业务流:用户注册 → 浏览商品 → 加入购物车 → 下单 → 支付 → 发货 → 完成。每个环节需要对应的表和约束。
(2) 关系说明
| 关系 | 类型 | 说明 |
|---|---|---|
| 用户 → 地址 | 一对多 | 一个用户可有多个收货地址 |
| 用户 → 订单 | 一对多 | 一个用户可下多个订单 |
| 订单 → 订单项 | 一对多 | 一个订单包含多个商品 |
| 商品 → 订单项 | 一对多 | 一个商品可出现在多个订单 |
| 分类 → 分类 | 自引用 | parent_id 实现无限级分类 |
| 分类 → 商品 | 一对多 | 一个分类下多个商品 |
| 商品 → 图片 | 一对多 | 一个商品多张图片 |
| 订单 → 支付 | 一对一 | 一个订单对应一条支付记录 |
▶ 示例:创建数据库
SQL
CREATE DATABASE IF NOT EXISTS ecommerce
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE ecommerce;
3. 用户系统
(1) 表结构设计
SQL
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
phone VARCHAR(20),
avatar VARCHAR(255) DEFAULT '/images/default_avatar.png',
status ENUM('active', 'inactive', 'banned') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_users_phone (phone),
INDEX idx_users_status (status)
) ENGINE=InnoDB COMMENT='用户表';
CREATE TABLE addresses (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
receiver VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
province VARCHAR(30) NOT NULL,
city VARCHAR(30) NOT NULL,
district VARCHAR(30) NOT NULL,
detail VARCHAR(200) NOT NULL,
is_default TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_addresses_user (user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='收货地址表';
CREATE TABLE user_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
action VARCHAR(50) NOT NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
detail JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_logs_user (user_id),
INDEX idx_logs_action (action),
INDEX idx_logs_created (created_at)
) ENGINE=InnoDB COMMENT='用户操作日志表';
(2) 注册与登录验证
▶ 示例:用户注册
SQL
INSERT INTO users (username, email, password_hash, phone)
VALUES (
'alice',
'alice@example.com',
SHA2('MySecret123!', 256),
'13800138000'
);
INSERT INTO users (username, email, password_hash, phone)
VALUES (
'bob_dev',
'bob@example.com',
SHA2('BobPass456!', 256),
'13900139000'
);
▶ 示例:登录验证
SQL
SELECT id, username, status FROM users
WHERE email = 'alice@example.com'
AND password_hash = SHA2('MySecret123!', 256)
AND status = 'active';
▶ 示例:地址管理
SQL
INSERT INTO addresses (user_id, receiver, phone, province, city, district, detail, is_default)
VALUES (1, 'Alice', '13800138000', 'Guangdong', 'Shenzhen', 'Nanshan', 'Tech Park Bldg 5 Room 301', 1);
INSERT INTO addresses (user_id, receiver, phone, province, city, district, detail, is_default)
VALUES (1, 'Alice', '13800138000', 'Beijing', 'Beijing', 'Haidian', 'Zhongguancun St 88', 0);
SELECT * FROM addresses WHERE user_id = 1 ORDER BY is_default DESC;
▶ 示例:用户操作日志
SQL
INSERT INTO user_logs (user_id, action, ip_address, user_agent, detail)
VALUES (1, 'LOGIN', '192.168.1.100', 'Chrome/120', '{"method": "email"}');
INSERT INTO user_logs (user_id, action, ip_address, detail)
VALUES (1, 'UPDATE_ADDRESS', '192.168.1.100', '{"address_id": 1}');
SELECT action, COUNT(*) AS cnt
FROM user_logs
WHERE user_id = 1
GROUP BY action
ORDER BY cnt DESC;
4. 商品系统
(1) 无限级分类
SQL
CREATE TABLE categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
sort_order INT DEFAULT 0,
icon VARCHAR(255),
is_visible TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_categories_parent (parent_id),
INDEX idx_categories_sort (sort_order)
) ENGINE=InnoDB COMMENT='商品分类表';
▶ 示例:分类数据
SQL
INSERT INTO categories (id, name, parent_id, sort_order) VALUES
(1, 'Electronics', 0, 1),
(2, 'Clothing', 0, 2),
(3, 'Home & Living', 0, 3),
(4, 'Smartphones', 1, 1),
(5, 'Laptops', 1, 2),
(6, 'Men', 2, 1),
(7, 'Women', 2, 2),
(8, 'Kitchen', 3, 1);
SELECT c1.name AS category, c2.name AS sub_category
FROM categories c1
LEFT JOIN categories c2 ON c1.id = c2.parent_id
WHERE c1.parent_id = 0
ORDER BY c1.sort_order, c2.sort_order;
(2) 商品与图片
SQL
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL CHECK (price > 0),
original_price DECIMAL(10,2),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
sales INT NOT NULL DEFAULT 0,
category_id BIGINT NOT NULL,
status ENUM('active', 'inactive', 'sold_out') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_products_category (category_id),
INDEX idx_products_status (status),
INDEX idx_products_sales (sales DESC),
INDEX idx_products_created (created_at DESC),
FOREIGN KEY (category_id) REFERENCES categories(id)
) ENGINE=InnoDB COMMENT='商品表';
CREATE TABLE product_images (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
image_url VARCHAR(255) NOT NULL,
is_main TINYINT(1) DEFAULT 0,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_images_product (product_id),
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='商品图片表';
▶ 示例:商品 CRUD
SQL
INSERT INTO products (name, description, price, original_price, stock, category_id, status)
VALUES
('Galaxy S24 Ultra', 'Samsung flagship 2024', 1299.99, 1399.99, 200, 4, 'active'),
('MacBook Pro 16', 'Apple M3 Max laptop', 2499.00, 2499.00, 50, 5, 'active'),
('Cotton T-Shirt', 'Pure cotton casual tee', 29.99, 39.99, 500, 6, 'active'),
('Smart Rice Cooker', 'IoT enabled cooker', 89.99, 119.99, 300, 8, 'active');
INSERT INTO product_images (product_id, image_url, is_main, sort_order) VALUES
(1, '/images/galaxy_s24_1.jpg', 1, 1),
(1, '/images/galaxy_s24_2.jpg', 0, 2),
(2, '/images/macbook_pro_1.jpg', 1, 1),
(3, '/images/tshirt_white.jpg', 1, 1),
(4, '/images/cooker_main.jpg', 1, 1);
SELECT p.id, p.name, p.price, pi.image_url
FROM products p
LEFT JOIN product_images pi ON p.id = pi.product_id AND pi.is_main = 1
WHERE p.category_id = 4 AND p.status = 'active'
ORDER BY p.sales DESC
LIMIT 10;
UPDATE products SET stock = stock - 1, sales = sales + 1 WHERE id = 1;
UPDATE products SET status = 'sold_out' WHERE stock <= 0;
5. 订单系统
(1) 订单与订单项
SQL
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0,
pay_amount DECIMAL(12,2) DEFAULT NULL,
status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled', 'refunded')
DEFAULT 'pending',
receiver VARCHAR(50) NOT NULL,
receiver_phone VARCHAR(20) NOT NULL,
shipping_address TEXT NOT NULL,
remark VARCHAR(500),
paid_at DATETIME DEFAULT NULL,
shipped_at DATETIME DEFAULT NULL,
completed_at DATETIME DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_orders_user (user_id),
INDEX idx_orders_status (status),
INDEX idx_orders_created (created_at DESC),
INDEX idx_orders_order_no (order_no),
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB COMMENT='订单表';
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(200) NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
price DECIMAL(10,2) NOT NULL,
subtotal DECIMAL(12,2) GENERATED ALWAYS AS (quantity * price) STORED,
INDEX idx_items_order (order_id),
INDEX idx_items_product (product_id),
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB COMMENT='订单明细表';
(2) 订单状态流转
| 当前状态 | 可转换状态 | 触发条件 |
|---|---|---|
| pending | paid | 支付成功 |
| pending | cancelled | 用户取消/超时未付 |
| paid | shipped | 商家发货 |
| paid | refunded | 申请退款 |
| shipped | completed | 买家确认收货 |
| completed | refunded | 售后退款 |
▶ 示例:下单存储过程(含库存扣减+事务)
SQL
📖 仅展示
DELIMITER //
CREATE PROCEDURE sp_create_order(
IN p_user_id BIGINT,
IN p_receiver VARCHAR(50),
IN p_phone VARCHAR(20),
IN p_address TEXT,
IN p_remark VARCHAR(500)
)
BEGIN
DECLARE v_order_no VARCHAR(32);
DECLARE v_total DECIMAL(12,2) DEFAULT 0;
DECLARE v_cart_item INT;
DECLARE v_product_id BIGINT;
DECLARE v_quantity INT;
DECLARE v_price DECIMAL(10,2);
DECLARE v_stock INT;
DECLARE v_name VARCHAR(200);
DECLARE done INT DEFAULT 0;
DECLARE cart_cursor CURSOR FOR
SELECT c.product_id, c.quantity, p.price, p.name, p.stock
FROM cart c
JOIN products p ON c.product_id = p.id
WHERE c.user_id = p_user_id AND p.status = 'active'
FOR UPDATE;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
START TRANSACTION;
SET v_order_no = CONCAT(
'ORD', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'),
LPAD(FLOOR(RAND() * 10000), 4, '0')
);
INSERT INTO orders (order_no, user_id, receiver, receiver_phone, shipping_address, remark)
VALUES (v_order_no, p_user_id, p_receiver, p_phone, p_address, p_remark);
OPEN cart_cursor;
read_loop: LOOP
FETCH cart_cursor INTO v_product_id, v_quantity, v_price, v_name, v_stock;
IF done THEN LEAVE read_loop; END IF;
IF v_stock < v_quantity THEN
ROLLBACK;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = CONCAT('Insufficient stock for: ', v_name);
END IF;
INSERT INTO order_items (order_id, product_id, product_name, quantity, price)
VALUES (LAST_INSERT_ID(), v_product_id, v_name, v_quantity, v_price);
UPDATE products
SET stock = stock - v_quantity, sales = sales + v_quantity
WHERE id = v_product_id;
SET v_total = v_total + v_quantity * v_price;
END LOOP;
CLOSE cart_cursor;
UPDATE orders SET total_amount = v_total WHERE order_no = v_order_no;
DELETE FROM cart WHERE user_id = p_user_id;
COMMIT;
END //
DELIMITER ;
▶ 示例:调用下单
SQL
CALL sp_create_order(
1,
'Alice',
'13800138000',
'Tech Park Bldg 5 Room 301, Nanshan, Shenzhen, Guangdong',
'Please deliver before noon'
);
▶ 示例:订单状态更新
SQL
UPDATE orders SET status = 'paid', pay_amount = total_amount, paid_at = NOW()
WHERE id = 1 AND status = 'pending';
UPDATE orders SET status = 'shipped', shipped_at = NOW()
WHERE id = 1 AND status = 'paid';
UPDATE orders SET status = 'completed', completed_at = NOW()
WHERE id = 1 AND status = 'shipped';
6. 购物车
(1) 表结构
SQL
CREATE TABLE cart (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1 CHECK (quantity > 0),
selected TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_cart_user_product (user_id, product_id),
INDEX idx_cart_user (user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='购物车表';
▶ 示例:购物车增删改查
SQL
INSERT INTO cart (user_id, product_id, quantity) VALUES (1, 1, 2);
INSERT INTO cart (user_id, product_id, quantity) VALUES (1, 2, 1)
ON DUPLICATE KEY UPDATE quantity = quantity + 1;
UPDATE cart SET quantity = 3 WHERE user_id = 1 AND product_id = 1;
DELETE FROM cart WHERE user_id = 1 AND product_id = 2;
SELECT c.product_id, p.name, p.price, c.quantity,
p.price * c.quantity AS subtotal
FROM cart c
JOIN products p ON c.product_id = p.id
WHERE c.user_id = 1 AND c.selected = 1;
▶ 示例:合并购物车
SQL
DELIMITER //
CREATE PROCEDURE sp_merge_cart(
IN p_source_user_id BIGINT,
IN p_target_user_id BIGINT
)
BEGIN
INSERT INTO cart (user_id, product_id, quantity, selected)
SELECT p_target_user_id, product_id, quantity, selected
FROM cart
WHERE user_id = p_source_user_id
ON DUPLICATE KEY UPDATE
quantity = cart.quantity + VALUES(quantity),
selected = 1;
DELETE FROM cart WHERE user_id = p_source_user_id;
END //
DELIMITER ;
CALL sp_merge_cart(2, 1);
7. 支付记录
(1) 表结构
SQL
CREATE TABLE payments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
payment_no VARCHAR(64) NOT NULL UNIQUE,
amount DECIMAL(12,2) NOT NULL,
method ENUM('credit_card', 'debit_card', 'paypal', 'bank_transfer') NOT NULL,
status ENUM('pending', 'success', 'failed', 'refunded') DEFAULT 'pending',
paid_at DATETIME DEFAULT NULL,
refund_no VARCHAR(64) DEFAULT NULL,
refund_amount DECIMAL(12,2) DEFAULT NULL,
refund_reason VARCHAR(500) DEFAULT NULL,
refunded_at DATETIME DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_payments_order (order_id),
INDEX idx_payments_no (payment_no),
INDEX idx_payments_status (status),
FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB COMMENT='支付记录表';
(2) 支付状态流转
| 当前状态 | 可转换状态 | 触发条件 |
|---|---|---|
| pending | success | 支付网关回调成功 |
| pending | failed | 支付网关回调失败 |
| success | refunded | 发起退款 |
▶ 示例:支付操作
SQL
INSERT INTO payments (order_id, payment_no, amount, method, status, paid_at)
VALUES (1, CONCAT('PAY', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(FLOOR(RAND()*10000),4,'0')),
3899.97, 'credit_card', 'success', NOW());
UPDATE orders SET status = 'paid', pay_amount = 3899.97, paid_at = NOW()
WHERE id = 1 AND status = 'pending';
SELECT p.payment_no, p.amount, p.method, p.status, p.paid_at
FROM payments p
WHERE p.order_id = 1;
▶ 示例:退款操作
SQL
UPDATE payments
SET status = 'refunded',
refund_no = CONCAT('REF', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(FLOOR(RAND()*10000),4,'0')),
refund_amount = 3899.97,
refund_reason = 'Defective product',
refunded_at = NOW()
WHERE order_id = 1 AND status = 'success';
UPDATE orders SET status = 'refunded' WHERE id = 1;
8. 数据统计视图
▶ 示例:热销商品视图
SQL
CREATE OR REPLACE VIEW v_hot_products AS
SELECT
p.id,
p.name,
p.price,
c.name AS category,
p.sales,
IFNULL(SUM(oi.quantity), 0) AS order_sold,
IFNULL(SUM(oi.quantity * oi.price), 0) AS revenue
FROM products p
LEFT JOIN order_items oi ON p.id = oi.product_id
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.status = 'active'
GROUP BY p.id, p.name, p.price, c.name, p.sales
ORDER BY p.sales DESC;
SELECT * FROM v_hot_products LIMIT 10;
▶ 示例:用户消费排行
SQL
CREATE OR REPLACE VIEW v_user_ranking AS
SELECT
u.id,
u.username,
COUNT(DISTINCT o.id) AS total_orders,
IFNULL(SUM(o.total_amount), 0) AS total_spent,
IFNULL(AVG(o.total_amount), 0) AS avg_order_amount,
MAX(o.created_at) AS last_order_time
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status != 'cancelled'
GROUP BY u.id, u.username
ORDER BY total_spent DESC;
SELECT * FROM v_user_ranking LIMIT 20;
▶ 示例:月度销售报表
SQL
CREATE OR REPLACE VIEW v_monthly_sales AS
SELECT
DATE_FORMAT(o.created_at, '%Y-%m') AS month,
COUNT(o.id) AS order_count,
SUM(o.total_amount) AS revenue,
SUM(CASE WHEN o.status = 'completed' THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN o.status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE WHEN o.status = 'refunded' THEN 1 ELSE 0 END) AS refunded,
ROUND(
SUM(CASE WHEN o.status = 'completed' THEN 1 ELSE 0 END) / COUNT(o.id) * 100,
2
) AS completion_rate
FROM orders o
GROUP BY DATE_FORMAT(o.created_at, '%Y-%m')
ORDER BY month DESC;
SELECT * FROM v_monthly_sales;
9. 权限管理与备份策略
(1) 权限矩阵
| 用户 | SELECT | INSERT | UPDATE | DELETE | EXECUTE | ALTER | DROP | CREATE |
|---|---|---|---|---|---|---|---|---|
| ecom_readonly | Y | - | - | - | - | - | - | - |
| ecom_app | Y | Y | Y | - | Y | - | - | - |
| ecom_admin | Y | Y | Y | Y | Y | Y | Y | Y |
▶ 示例:创建权限用户
SQL
CREATE USER 'ecom_readonly'@'%' IDENTIFIED BY 'ReadOnly2024!';
GRANT SELECT ON ecommerce.* TO 'ecom_readonly'@'%';
CREATE USER 'ecom_app'@'%' IDENTIFIED BY 'AppUser2024!';
GRANT SELECT, INSERT, UPDATE ON ecommerce.* TO 'ecom_app'@'%';
GRANT EXECUTE ON ecommerce.* TO 'ecom_app'@'%';
CREATE USER 'ecom_admin'@'127.0.0.1' IDENTIFIED BY 'Admin2024!Secure';
GRANT ALL PRIVILEGES ON ecommerce.* TO 'ecom_admin'@'127.0.0.1';
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'ecom_app'@'%';
(2) 备份策略
| 备份类型 | 频率 | 保留 | 工具 |
|---|---|---|---|
| 全量备份 | 每日 02:00 | 30 天 | mysqldump |
| 二进制日志 | 实时 | 7 天 | binlog |
| 表结构备份 | 每周日 | 90 天 | mysqldump --no-data |
▶ 示例:备份脚本
BASH
#!/bin/bash
BACKUP_DIR="/backup/mysql"
DB_NAME="ecommerce"
DB_USER="ecom_admin"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
mysqldump -u $DB_USER -pAdmin2024!Secure \
--single-transaction \
--routines \
--triggers \
--views \
$DB_NAME | gzip > ${BACKUP_DIR}/${DB_NAME}_full_${DATE}.sql.gz
mysqldump -u $DB_USER -pAdmin2024!Secure \
--no-data \
$DB_NAME | gzip > ${BACKUP_DIR}/${DB_NAME}_schema_${DATE}.sql.gz
find $BACKUP_DIR -name "${DB_NAME}_full_*.sql.gz" -mtime +30 -delete
find $BACKUP_DIR -name "${DB_NAME}_schema_*.sql.gz" -mtime +90 -delete
echo "Backup completed: ${DATE}"
▶ 示例:恢复数据
BASH
gunzip < /backup/mysql/ecommerce_full_20240115_020000.sql.gz | mysql -u ecom_admin -p ecommerce
10. 索引设计清单
| 表 | 索引名 | 列 | 类型 | 用途 |
|---|---|---|---|---|
| users | idx_users_phone | phone | BTREE | 手机号登录 |
| users | idx_users_status | status | BTREE | 状态过滤 |
| addresses | idx_addresses_user | user_id | BTREE | 用户地址列表 |
| categories | idx_categories_parent | parent_id | BTREE | 查找子分类 |
| products | idx_products_category | category_id | BTREE | 分类商品列表 |
| products | idx_products_status | status | BTREE | 上架状态过滤 |
| products | idx_products_sales | sales DESC | BTREE | 热销排序 |
| products | idx_products_created | created_at DESC | BTREE | 新品排序 |
| orders | idx_orders_user | user_id | BTREE | 用户订单列表 |
| orders | idx_orders_status | status | BTREE | 状态过滤 |
| orders | idx_orders_created | created_at DESC | BTREE | 时间排序 |
| orders | idx_orders_order_no | order_no | UNIQUE | 订单号查询 |
| order_items | idx_items_order | order_id | BTREE | 订单明细 |
| order_items | idx_items_product | product_id | BTREE | 商品销量统计 |
| cart | uk_cart_user_product | user_id,product_id | UNIQUE | 购物车去重 |
| payments | idx_payments_order | order_id | BTREE | 订单支付查询 |
| payments | idx_payments_status | status | BTREE | 支付状态过滤 |
| user_logs | idx_logs_user | user_id | BTREE | 用户日志 |
| user_logs | idx_logs_created | created_at | BTREE | 时间范围查询 |
❓ 常见问题
Q 订单号怎么保证唯一?
A 用时间戳 + 随机数拼接(如
ORD202401151430520387),或使用 UUID,或依赖 Redis 自增序列。下单存储过程中用 CONCAT + DATE_FORMAT + RAND 生成,配合 UNIQUE 约束兜底。Q 库存超卖怎么办?
A 两种方案——悲观锁用
SELECT ... FOR UPDATE 锁定库存行,事务内扣减;乐观锁用版本号 UPDATE products SET stock = stock - N, version = version + 1 WHERE id = X AND version = V。高并发场景建议乐观锁 + Redis 预扣减。Q 订单表数据量太大?
A 三种策略——1. 按月分表
orders_202401、orders_202402;2. MySQL 分区 PARTITION BY RANGE (TO_DAYS(created_at));3. 冷热分离,超过 1 年的订单归档到历史表。查询时加时间条件命中分区。Q 分类怎么设计无限级?
A 用
parent_id 自引用外键,顶级分类 parent_id = 0。查询子树用递归 CTE:WITH RECURSIVE cat_tree AS (SELECT * FROM categories WHERE id = 1 UNION ALL SELECT c.* FROM categories c JOIN cat_tree ct ON c.parent_id = ct.id) SELECT * FROM cat_tree;Q 购物车存数据库还是 Redis?
A 登录用户购物车存数据库(持久化),同时缓存到 Redis(读写快)。未登录用户购物车存 Redis/LocalStorage,登录时合并到数据库。购物车操作频繁,Redis 减轻数据库压力。
Q 密码为什么要用 SHA2 而不是 MD5?
A MD5 已被证明存在碰撞漏洞,容易被彩虹表破解。SHA2-256 更安全,生产环境建议用 bcrypt 或 Argon2,它们自带盐值且可调计算成本。
Q order_items 为什么要冗余 product_name?
A 商品名称可能修改,但订单快照应保留下单时的信息。这是典型的反范式设计——牺牲一点存储换取数据一致性,避免商品改名后历史订单显示错误。
📖 小节
- 需求分析是起点,ER 图梳理实体与关系,8 模块覆盖电商全流程
- 用户系统三表协作:users 管身份,addresses 管收货,user_logs 管审计
- 商品系统用
parent_id自引用实现无限级分类,product_images支持多图 - 订单系统是核心:存储过程封装下单逻辑,
FOR UPDATE防超卖,事务保证原子性 - 购物车用
ON DUPLICATE KEY UPDATE实现加购合并,存储过程处理跨用户合并 - 支付记录独立于订单,状态流转清晰,退款有完整字段追溯
- 数据统计用视图封装复杂聚合查询,热销/排行/月报一键获取
- 权限与备份是安全底线:最小权限原则 + 每日全量备份 + binlog 增量恢复
📝 作业
-
基础题(难度⭐):创建 ecommerce 数据库,完成 users、addresses、categories、products 四张表的建表 SQL 并插入测试数据。
-
进阶题(难度⭐⭐):实现完整的购物车功能——建表、增删改查 SQL、编写
sp_merge_cart存储过程,并用ON DUPLICATE KEY UPDATE处理重复加购。 -
实战题(难度⭐⭐⭐):实现下单存储过程
sp_create_order,要求包含购物车遍历、库存检查(FOR UPDATE)、订单与明细插入、库存扣减、购物车清空,全程用事务包裹,库存不足时回滚并抛出错误。 -
分析题(难度⭐⭐⭐):创建三个统计视图(热销商品 / 用户消费排行 / 月度销售报表),编写查询验证视图结果,并分析当订单表超过 1000 万行时视图性能问题及优化方案。
-
挑战题(难度⭐⭐⭐⭐):完整部署 ShopEasy 电商数据库——建库建表、索引、存储过程、视图、权限用户(readonly/app/admin)、每日备份脚本,并编写一份 200 字的部署文档,说明初始化步骤与日常运维要点。