第 7 章 · 电商库表设计实战
本章目标:在 shop-db 项目中完成 shop_db 电商核心库的完整 DDL walkthrough;逐表讲解 categories、products、users、orders、order_lines、carts、cart_items 的字段含义、外键约束与索引策略;理解 slug + is_published + price(分) 字段约定;输出与 gin-web ch05 GORM 模型字段对照表,为 ch08~ch12 的 Java 访问层打基础。
学时建议:5~6 小时(含 2 小时 DDL 跟练与 ER 图绘制)
前置:完成 go-database ch01~ch06(SQL 查询、JOIN、事务、DDL、ER 与范式);了解基本电商业务名词。
7.1 场景说明:从 ER 图到可执行 DDL
ch06 已将 shop_db 抽象为 ER 模型。本章把模型落地为 MySQL 8 可执行 DDL,作为 shop-db 与后续 毕业项目 ch17 的 schema 基线。
| 业务实体 | 表名 | 与其它模块关系 |
|---|---|---|
| 商品分类 | categories | gin-web gorm.Model Category |
| 商品 | products | gin-web gorm.Model Product(slug/is_published) |
| 用户 | users | 简化版账户表 |
| 订单 | orders | gin-web 可扩展 Order |
| 订单行 | order_lines | 下单快照价 |
| 购物车 | carts / cart_items | go-dev ch06 Cart 概念持久化 |
说明:教学项目路径~/go-learn/shop-db;数据库名shop_db;对外 API 域名https://api.example.com仅为占位,不涉及任何企业内部仓库。
字段演进约定(全模块统一):
| 旧字段(gin-web ch05 示例) | 正式字段(shop-db ch07+) | 说明 |
|---|---|---|
sku | slug | URL 友好标识,如 wireless-mouse |
active / is_active | is_published | 是否上架展示 |
price DECIMAL(10,2) / BigDecimal | price BIGINT(分) | 以分存储,避免浮点误差 |
7.2 全局 ER 关系图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ categories │ 1 N │ products │ N M │ tags │ (ch13 扩展,本章预留)
│ id, slug │◄──────│ category_id │ │ (可选) │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌───────────────────┼───────────────────┐
│ N │ N │ N
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ cart_items │ │ order_lines │ │ (reviews) │ ch17 扩展
│ cart_id │ │ order_id │ └─────────────┘
│ product_id │ │ product_id │
└──────┬──────┘ └──────┬──────┘
│ N │ N
▼ 1 ▼ 1
┌─────────────┐ ┌─────────────┐
│ carts │ │ orders │
│ user_id │ │ user_id │
└──────┬──────┘ └──────┬──────┘
│ N │ N
└─────────┬─────────┘
▼ 1
┌─────────────┐
│ users │
│ id, email │
└─────────────┘
关系摘要:
| 关系 | 类型 | 外键位置 | ON DELETE 建议 |
|---|---|---|---|
| Category → Product | 1:N | products.category_id | RESTRICT(有商品禁删分类) |
| User → Order | 1:N | orders.user_id | RESTRICT |
| Order → OrderLine | 1:N | order_lines.order_id | CASCADE |
| Product → OrderLine | 1:N | order_lines.product_id | RESTRICT(保历史单价) |
| User → Cart | 1:1 | carts.user_id UNIQUE | CASCADE |
| Cart → CartItem | 1:N | cart_items.cart_id | CASCADE |
| Product → CartItem | 1:N | cart_items.product_id | RESTRICT |
7.3 建库与字符集
MySQL 8(shop-db 默认):
CREATE DATABASE IF NOT EXISTS shop_db
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE shop_db;
Docker 快速启动(可选):
docker run -d --name shop-mysql \
-e MYSQL_ROOT_PASSWORD=root_pass \
-e MYSQL_DATABASE=shop_db \
-e MYSQL_USER=shop_user \
-e MYSQL_PASSWORD=shop_pass \
-p 3306:3306 \
mysql:8.0
| 配置项 | 教学值 | 说明 |
|---|---|---|
| 库名 | shop_db | 与 python-database db-demo 同名便于对照 |
| 字符集 | utf8mb4 | 支持 emoji 与完整 Unicode |
| 引擎 | InnoDB | 支持事务与外键 |
7.4 表 1:categories(商品分类)
CREATE TABLE IF NOT EXISTS categories (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
slug VARCHAR(64) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_categories_slug (slug),
KEY idx_categories_sort (sort_order, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
id | BIGINT | PK, AUTO | surrogate 主键 |
name | VARCHAR(64) | NOT NULL | 展示名,如「数码配件」 |
slug | VARCHAR(64) | NOT NULL, UNIQUE | URL 段,如 digital-accessories |
sort_order | INT | DEFAULT 0 | 前台排序,越小越靠前 |
created_at | DATETIME(3) | DEFAULT now | 毫秒精度审计 |
updated_at | DATETIME(3) | ON UPDATE | 应用层或 DB 自动维护 |
设计要点:
slug全局唯一,供/catalog/{slug}/路由使用。- 不在分类表存商品数量;用
COUNT(*)或冗余字段(ch14 优化再议)。
7.5 表 2:products(商品)
CREATE TABLE IF NOT EXISTS products (
id BIGINT NOT NULL AUTO_INCREMENT,
category_id BIGINT NOT NULL,
title VARCHAR(128) NOT NULL,
slug VARCHAR(128) NOT NULL,
description TEXT NULL,
price BIGINT NOT NULL COMMENT '单价,单位:分',
stock INT NOT NULL DEFAULT 0,
is_published TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_products_slug (slug),
KEY idx_products_category_published (category_id, is_published, created_at DESC),
KEY idx_products_published_price (is_published, price),
CONSTRAINT fk_products_category
FOREIGN KEY (category_id) REFERENCES categories(id)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT chk_products_price CHECK (price >= 0),
CONSTRAINT chk_products_stock CHECK (stock >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
category_id | BIGINT | FK → categories | 所属分类 |
title | VARCHAR(128) | NOT NULL | 商品标题 |
slug | VARCHAR(128) | UNIQUE | 商品 URL 标识 |
description | TEXT | 可空 | 详情 HTML/Markdown |
price | BIGINT | ≥0 | 单价(分),5990 = ¥59.90 |
stock | INT | ≥0 | 可售库存 |
is_published | TINYINT(1) | 0/1 | 1=上架,0=草稿/下架 |
价格(分)示例:
| 展示价 | 存储值 price | Java 转换 |
|---|---|---|
| ¥0.01 | 1 | priceCents / 100.0 展示用 |
| ¥59.90 | 5990 | Math.round(yuan * 100) 或 BigDecimal |
| ¥199.00 | 19900 | 禁止用 double 直接存库 |
索引策略 §7.5.1:
| 索引名 | 列 | 服务查询 |
|---|---|---|
idx_products_category_published | (category_id, is_published, created_at DESC) | 分类页已上架商品列表 |
idx_products_published_price | (is_published, price) | 全站按价排序、筛选 |
uk_products_slug | slug | 详情页 /products/{slug}/ |
7.6 表 3:users(用户)
CREATE TABLE IF NOT EXISTS users (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(64) NOT NULL DEFAULT '',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_users_email (email),
KEY idx_users_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| 字段 | 说明 |
|---|---|
email | 登录账号,唯一 |
password_hash | bcrypt/argon2 摘要,禁止存明文 |
nickname | 展示名,如 user-demo |
is_active | 0=禁用,1=正常 |
7.7 表 4:orders 与 order_lines(订单)
7.7.1 orders
CREATE TABLE IF NOT EXISTS orders (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_no VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
total_cents BIGINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_orders_order_no (order_no),
KEY idx_orders_user_created (user_id, created_at DESC),
KEY idx_orders_status (status, created_at DESC),
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT chk_orders_status
CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
CONSTRAINT chk_orders_total CHECK (total_cents >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
| 字段 | 说明 |
|---|---|
order_no | 业务单号,如 ORD-20260819-0001,唯一 |
status | 状态机:pending → paid → shipped;cancelled 终态 |
total_cents | 订单总金额(分),可与明细汇总校验 |