MySQL: 综合项目:电商数据库设计与实现

最后更新:2026-08-26

Alice 创办的电商平台 ShopEasy 刚拿到融资,需要从 Excel 管理升级到 MySQL。她从零设计了完整的电商数据库,8 个模块覆盖从用户注册到数据分析的全流程。上线后日订单 5,000+ 笔,数据库稳如磐石。

1. 项目总览

(1) 模块与表清单

模块 核心表 主要功能
用户系统 users / addresses / user_logs 注册登录、地址管理、操作审计
商品系统 categories / products / product_images 无限级分类、商品CRUD、多图
订单系统 orders / order_items 下单、库存扣减、状态流转
购物车 cart 增删改查、合并购物车
支付记录 payments 支付状态、退款
数据统计 视图 热销商品、消费排行、月度报表
权限管理 MySQL用户 只读/应用/管理员
备份策略 mysqldump 每日备份、保留策略

(2) 完整 ER 图

100%
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 ;
逻辑代码 54 行(超过 40 行限制,仅展示)

▶ 示例:调用下单

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_202401orders_202402;2. MySQL 分区 PARTITION BY RANGE (TO_DAYS(created_at));3. 冷热分离,超过 1 年的订单归档到历史表。查询时加时间条件命中分区。
Q 分类怎么设计无限级?
Aparent_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 商品名称可能修改,但订单快照应保留下单时的信息。这是典型的反范式设计——牺牲一点存储换取数据一致性,避免商品改名后历史订单显示错误。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 ecommerce 数据库,完成 users、addresses、categories、products 四张表的建表 SQL 并插入测试数据。

  2. 进阶题(难度⭐⭐):实现完整的购物车功能——建表、增删改查 SQL、编写 sp_merge_cart 存储过程,并用 ON DUPLICATE KEY UPDATE 处理重复加购。

  3. 实战题(难度⭐⭐⭐):实现下单存储过程 sp_create_order,要求包含购物车遍历、库存检查(FOR UPDATE)、订单与明细插入、库存扣减、购物车清空,全程用事务包裹,库存不足时回滚并抛出错误。

  4. 分析题(难度⭐⭐⭐):创建三个统计视图(热销商品 / 用户消费排行 / 月度销售报表),编写查询验证视图结果,并分析当订单表超过 1000 万行时视图性能问题及优化方案。

  5. 挑战题(难度⭐⭐⭐⭐):完整部署 ShopEasy 电商数据库——建库建表、索引、存储过程、视图、权限用户(readonly/app/admin)、每日备份脚本,并编写一份 200 字的部署文档,说明初始化步骤与日常运维要点。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏