第 17 章 · 毕业项目 shop-db 设计与验收
本章目标:独立交付 shop-db Go 数据库毕业项目:完整 ER 图、DDL、golang-migrate 迁移链、GORM 实体 与 seed;全链路贯彻 slug + is_published + price(分) 契约;按 100 分验收表自评;编写 docs/ 衔接 gin-web api-go-demo-demo 的 GORM 对照说明;形成可放入作品集的数据库设计文档。
学时建议:6~8 小时(含 4 小时建模与迁移)
前置:完成 ch01~ch16;ch16 综合练习已通过;建议浏览 gin-web ch05 商品持久化章节。
17.1 项目背景与边界
shop-db 是虚构商城 贤紫优选 的 Go 数据库层毕业交付,为 gin-web 毕业项目提供 唯一真相源(SSOT):
┌─────────────────────────────────────────────────────────┐
│ shop-db(本章) │
│ ER · DDL · golang-migrate · GORM Entity · seed · docs │
└───────────────────────────┬─────────────────────────────┘
│ 同 ER / 同字段契约
▼
gin-web
api-go-demo-demo
Spring Data GORM + REST
| 模块 | shop-db 必须 | 不做(扩展) |
|---|---|---|
| 用户 | users 表 + slug | OAuth 第三方表 |
| 商品 | products slug/is_published/price分 | SKU 矩阵多规格 |
| 分类 | categories 树形 | 无限级性能优化 |
| 订单 | orders + order_items 快照 | 分库分表落地 |
| 购物车 | cart_items | 分布式购物车 |
| 迁移 | golang-migrate 全链 | 多 DB 联邦 |
严禁真实连接串、生产域名入库。统一 user-demo、api.example.com、本地 MySQL。
17.2 交付物清单
| # | 交付物 | 说明 |
|---|---|---|
| 1 | docs/ER.md | ER 图(mermaid 或图片)+ 文字说明 |
| 2 | sql/schema.sql | 可独立执行的 DDL |
| 3 | src/main/resources/db/migration/V*.sql | ≥2 个版本(初始 + 增量) |
| 4 | src/main/go/.../catalog/*.go 等 | GORM 实体 |
| 5 | scripts/SeedMain.go 或 SQL seed | 样本数据 |
| 6 | docs/GORM_BRIDGE.md | gin-web 字段对照 |
| 7 | docs/SELF_REVIEW.md | 100 分自评表 |
| 8 | README.md | 安装、golang-migrate、seed 命令 |
17.3 推荐目录结构
~/go-learn/shop-db/
├── go.mod
├── sql/
│ └── schema.sql
├── src/main/go/com/zixian/shopdb/
│ ├── catalog/
│ │ ├── Category.go
│ │ ├── Product.go
│ │ └── ProductRepository.go
│ ├── order/
│ │ ├── ShopOrder.go
│ │ └── OrderItem.go
│ ├── user/
│ │ └── User.go
│ └── seed/
│ └── SeedMain.go
├── src/main/resources/
│ ├── META-INF/gorm.Open
│ └── db/migration/
│ ├── V1__initial_schema.sql
│ └── V2__add_product_view_count.sql
├── scripts/
│ └── run-flyway.sh
├── docs/
│ ├── ER.md
│ ├── GORM_BRIDGE.md
│ ├── SCALE.md ← ch15 扩展预案
│ ├── PERF.md ← ch14
│ └── SELF_REVIEW.md
└── README.md
17.4 ER 模型总览
17.4.1 实体关系(mermaid)
erDiagram
users ||--o{ orders : places
users ||--o{ cart_items : owns
categories ||--o{ products : contains
products ||--o{ cart_items : referenced
orders ||--|{ order_items : contains
products ||--o{ order_items : snapshot
users {
bigint id PK
varchar slug UK
varchar email UK
varchar password_hash
datetime created_at
}
categories {
bigint id PK
varchar slug UK
varchar name
bigint parent_id FK
}
products {
bigint id PK
varchar slug UK
varchar name
bigint price "分"
bool is_published
bigint category_id FK
bigint view_count
}
orders {
bigint id PK
varchar order_no UK
varchar user_slug
varchar status
bigint total_amount "分"
}
order_items {
bigint id PK
bigint order_id FK
varchar product_slug
bigint price "分"
int quantity
}
cart_items {
bigint id PK
varchar user_slug
bigint product_id FK
int quantity
}
17.4.2 设计原则
| 原则 | 实现 |
|---|---|
| 价格 | 全程 BIGINT 分 |
| 商品标识 | slug 对外;id 对内 FK |
| 上下架 | is_published bool |
| 订单快照 | order_items 冗余 slug/name/price |
| 用户 | user_slug 便于与 JWT 对齐 |
17.5 完整 DDL(schema.sql 摘要)
CREATE DATABASE IF NOT EXISTS shop_db
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE shop_db;
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(64) NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_users_slug (slug),
UNIQUE KEY uk_users_email (email)
) ENGINE=InnoDB;
CREATE TABLE categories (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(128) NOT NULL,
name VARCHAR(128) NOT NULL,
parent_id BIGINT NULL,
sort_order INT NOT NULL DEFAULT 0,
UNIQUE KEY uk_categories_slug (slug),
KEY idx_categories_parent (parent_id),
CONSTRAINT fk_categories_parent FOREIGN KEY (parent_id) REFERENCES categories(id)
) ENGINE=InnoDB;
CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(128) NOT NULL,
name VARCHAR(256) NOT NULL,
description TEXT NULL,
price BIGINT NOT NULL COMMENT '价格-分',
is_published TINYINT(1) NOT NULL DEFAULT 0,
category_id BIGINT NULL,
view_count BIGINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_products_slug (slug),
KEY idx_products_published_id (is_published, id DESC),
KEY idx_products_category_published (category_id, is_published, id DESC),
CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES categories(id)
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL,
user_slug VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
total_amount BIGINT NOT NULL COMMENT '订单总额-分',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_orders_order_no (order_no),
KEY idx_orders_user_created (user_slug, created_at DESC),
KEY idx_orders_created (created_at)
) ENGINE=InnoDB;
CREATE TABLE order_items (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_slug VARCHAR(128) NOT NULL,
product_name VARCHAR(256) NOT NULL,
price BIGINT NOT NULL COMMENT '成交单价-分',
quantity INT NOT NULL,
KEY idx_order_items_order (order_id),
CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB;
CREATE TABLE cart_items (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_slug VARCHAR(64) NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_cart_user_product (user_slug, product_id),
KEY idx_cart_user (user_slug),
CONSTRAINT fk_cart_product FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB;
17.6 GORM 实体(Product 核心)
gorm.Model
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 128)
private String slug;
@Column(nullable = false, length = 256)
private String name;
@Column(nullable = false)
private Long price; // 分
@Column(name = "is_published", nullable = false)
private boolean published;
@Column(name = "category_id")
private Long categoryId;