🥰个人主页:会编程的土豆(欢迎来访)
💎作者简介:后端学习者
✨那些你一个人走过的夜路,终将化作照亮未 来的光



系列:《影院票务 GO》知识点博客 · 第 6 篇
对应文件:
sql/schema.sql、sql/seed.sql,以及config/config.go里的 DSN
写在前面
票务系统所有「记得住」的东西------用户账号、电影、场次、座位状态、订单------最后都落在 MySQL 里。
Go 程序重启后,内存 Session 会丢;数据库里的订单还在。
学长可能问:
「库和表啥区别?为什么每张表都有 id?DECIMAL 和 FLOAT 有啥不一样?」
本篇用最直白的方式建立数据库世界观,并对照本项目的 ttms 库。
不涉及复杂 SQL 语法细节(那是第 7 篇),先把名词和表结构 讲透,能和 models/models.go 对上号。
1. 四级概念:从服务器到单元格
MySQL 服务器(进程,默认端口 3306)
└── 数据库 Database(如 ttms)
└── 表 Table(如 users、movies、orders)
└── 行 Row(一条用户、一笔订单)
└── 列 Column(username、amount、status...)
| 概念 | 类比 | 项目例子 |
|---|---|---|
| 服务器 | 整栋图书馆大楼 | 你电脑上的 MySQL 8 服务 |
| 数据库 | 一个分馆 / 一个项目专属书架区 | ttms |
| 表 | 一种登记册 | users、seats、orders |
| 行 | 登记册里的一行 | 用户 admin 那一行 |
| 列 | 登记项 | username、password、role |
脚本开头(schema.sql):
DROP DATABASE IF EXISTS ttms;
CREATE DATABASE ttms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ttms;
含义:
- 若已有
ttms库则删掉(开发脚本危险操作,生产绝不用) - 创建新库,指定字符集
USE ttms进入该库,后面CREATE TABLE都建在这里
和 Go 的衔接:
config.Load() 默认 DSN 末尾是 /ttms?charset=utf8mb4&...------连的就是这个库名。
2. 为什么需要数据库?不用文件行吗?
可以把订单写进 orders.txt。小 demo 可以,票务系统不行,因为数据库擅长:
| 能力 | 文件方案的痛点 |
|---|---|
| 结构化查询 | 找「某用户所有待支付订单」要扫全文 |
| 并发安全 | 两人同时买同一座,文件很难原子更新 |
| 约束 | 用户名重复、外键引用靠自觉 |
| 持久与恢复 | 文件也能持久,但缺少事务、备份生态 |
| 索引 | 按时间查场次,表上有 INDEX |
课程项目必须用 MySQL,也是为了练 事务、行锁、外键------后面第 16、17 篇的主线。
3. 本项目表关系鸟瞰
schema.sql 建的表(简化关系):
users ─────┬──── orders ──── schedules ──── movies
│ │
│ └── halls
├── comments ──── movies
├── ratings ──── movies
│
movie_groups ── movies
schedules ── seats(每场次一批座位)
| 表 | 存什么 |
|---|---|
users |
登录账号、角色 |
movie_groups |
动作/科幻等分类 |
movies |
电影元数据、评分汇总 |
halls |
影厅名、行列数 |
schedules |
哪部电影、哪个厅、几点、票价 |
seats |
某场次下每个格子的状态 |
orders |
订单号、座位摘要、金额、支付截止 |
comments |
评论与回复(parent_id) |
ratings |
用户对电影 1--10 分 |
Go 里 models 包与这些表一一对应(第 5 篇)。
4. 主键 PRIMARY KEY
几乎每张表都有:
id BIGINT PRIMARY KEY AUTO_INCREMENT
| 概念 | 含义 |
|---|---|
| 主键 | 唯一标识一行,不能重复、不能 NULL(惯例) |
| AUTO_INCREMENT | 插入时不写 id,MySQL 自动分配 1, 2, 3... |
| BIGINT | 64 位整数,id 空间大 |
为什么几乎都用 surrogate key(代理主键 id)?
- 关联方便 :
orders.user_id→users.id - 业务字段会变:昵称可改,username 理论上也可改,id 稳定
- Join 性能:整数比长字符串做主键更常见
拓展:自然键?
用 username 或 order_no 当主键理论上可以,但 order_no 在本项目仍是 UNIQUE 业务键 ,主键仍是 id------两套并存(见思考题)。
5. 常见列类型(对照 schema)
5.1 字符串
username VARCHAR(50) NOT NULL UNIQUE
password CHAR(32) NOT NULL
description TEXT
seat_info VARCHAR(255)
| 类型 | 特点 | 项目用途 |
|---|---|---|
| VARCHAR(n) | 可变长,最多 n 字符 | 用户名、标题 |
| CHAR(n) | 定长,不足补空格(存比较时) | MD5 十六进制固定 32 位 |
| TEXT | 长文本 | 电影简介 |
5.2 数值
role TINYINT NOT NULL DEFAULT 0
duration INT NOT NULL DEFAULT 120
amount DECIMAL(10,2) NOT NULL DEFAULT 0
rating_avg DECIMAL(3,1) NOT NULL DEFAULT 0.0
| 类型 | 用途 |
|---|---|
| TINYINT | 小整数、枚举状态(0/1/2) |
| INT | 时长分钟、计数 |
| DECIMAL(p,s) | 精确小数,金额、评分 |
钱为什么用 DECIMAL 不用 FLOAT?
- FLOAT/DOUBLE 是二进制浮点,
0.1 + 0.2在计算机里不精确 - 票价 45.00、订单 90.00 必须精确到分
- Go
Scan进float64对课程够用;金融系统常用整数「分」或 decimal 库
rating_avg DECIMAL(3,1):最多 3 位数字、1 位小数,如 9.3、10.0(思考题:最大值?)
5.3 时间
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
start_time DATETIME NOT NULL
pay_deadline DATETIME NOT NULL
paid_at DATETIME NULL
Go DSN 里 parseTime=true&loc=Local 才能把 DATETIME 扫进 time.Time。
6. 约束:数据库帮你守门
username VARCHAR(50) NOT NULL UNIQUE
| 约束 | 含义 |
|---|---|
| NOT NULL | 不能空 |
| UNIQUE | 不能重复 |
| DEFAULT | 缺省值 |
| PRIMARY KEY | 主键唯一 |
| FOREIGN KEY | 引用父表必须存在 |
例:订单表
CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_order_sch FOREIGN KEY (schedule_id) REFERENCES schedules(id),
插入 user_id=99999 若 users 里没有 → 拒绝,防止孤儿订单。
座位表:
UNIQUE KEY uk_seat (schedule_id, row_no, col_no),
同一场次同一排座不能重复两行------防数据脏。
应用层也要校验,但约束是最后一道防线。
7. 索引 INDEX(入门)
INDEX idx_sch_start (start_time),
INDEX idx_order_user (user_id),
INDEX idx_seat_lock (status, lock_until),
索引像「书的目录」:按用户查订单、按时间查场次、清理任务扫超时锁座,不必全表扫描。
代价: 插入/update 要维护索引;小项目数据量小感觉不明显,大数据量关键。
8. 引擎 InnoDB
) ENGINE=InnoDB;
每张表末尾都有。
| 引擎 | 特点 |
|---|---|
| InnoDB | 事务、行级锁、外键------购票必备 |
| MyISAM(老) | 无事务,不适合订单锁座 |
本项目全部 InnoDB 。
CreateOrderWithSeats 里 BEGIN + FOR UPDATE 依赖 InnoDB 行锁(第 17 篇)。
9. 字符集 utf8mb4
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
| 名称 | 说明 |
|---|---|
| utf8mb4 | 完整 UTF-8,含中文、emoji(4 字节) |
| utf8(MySQL 旧名) | 最多 3 字节,不完整 |
电影名、评论、昵称可能有 emoji;DSN 也要 charset=utf8mb4。
乱码排查:客户端编码、表字符集、连接 charset 三者一致。
10. schema vs seed
| 文件 | 作用 | 本项目特点 |
|---|---|---|
schema.sql |
建库建表(结构) | 开头 DROP DATABASE,会清空整个库 |
seed.sql |
插入演示数据(内容) | 默认账号、电影、场次 |
典型开发流程:
1. 改表结构 → 编辑 schema.sql → 在 MySQL 里执行
2. 需要演示数据 → 执行 seed.sql(依赖 schema 已建好)
3. 启动 Go → db.Init Ping ttms 成功
db.Init 失败时的提示:
连接 MySQL 失败: ...(请先执行 sql/schema.sql 和 sql/seed.sql)
说明:库不存在、表不存在、MySQL 没启动都会 Ping 失败。
10.1 seed 里有什么
- 管理员
admin/admin123、顾客user/user123(密码 MD5+盐,第 13 篇讲) - 5 部电影、3 个影厅、若干未来场次
- 执行 seed 后才有首页列表可点
11. 单表精读:users
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password CHAR(32) NOT NULL COMMENT 'MD5(password+salt)',
salt VARCHAR(16) NOT NULL,
nickname VARCHAR(50) NOT NULL DEFAULT '',
phone VARCHAR(20) NOT NULL DEFAULT '',
role TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
| 列 | 对应 models.User |
|---|---|
| id | ID int64 |
| username | Username |
| password | Password(json:"-") |
| role 0/1 | RoleCustomer / RoleAdmin |
12. 单表精读:seats 与 orders
seats------购票并发核心:
status TINYINT NOT NULL DEFAULT 0, -- 0空闲 1锁定 2已售
lock_user_id BIGINT NULL,
lock_until DATETIME NULL,
order_id BIGINT NULL,
和 models.Seat、SeatFree/Locked/Sold 常量一致。
orders:
order_no VARCHAR(32) NOT NULL UNIQUE,
seat_info VARCHAR(255) NOT NULL DEFAULT '',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 0,
pay_deadline DATETIME NOT NULL,
order_no 业务可见;id 内部关联 seats.order_id。
13. 和 Go 的三层连接(预告后续篇)
Go 应用
database/sql(标准库接口)
go-sql-driver/mysql(驱动,db 包 _ 导入)
MySQL 服务器 :3306
数据库 ttms
| 组件 | 文件 |
|---|---|
| DSN 字符串 | config/config.go |
sql.Open + Ping |
db/db.go |
| 执行 SQL | dao/*.go |
| 行 → struct | Scan 进 models.* |
本篇不讲 SELECT/INSERT 语法细节------那是 07 · SQL 增删改查 。
本篇不讲 连接池参数------那是 08 · database/sql。
14. 拓展:客户端怎么执行脚本
方式任选:
mysql -u root -p < sql/schema.sql
mysql -u root -p < sql/seed.sql
或在 Navicat / DBeaver / MySQL Workbench 里打开文件执行。
执行前确认连的是开发机,不是生产。
15. 常见误区
-
「数据库 = MySQL 软件」
常混:MySQL 是服务器软件;
ttms是它里面的一个 database。 -
「没有 schema 也能 go run」
db.InitPing 会失败,main Fatal 退出。 -
「FLOAT 存票价省空间」
省那点空间不值得丢精度。
-
「DROP DATABASE 和 DELETE 一样」
DROP 整个库没了;DELETE 删行。schema 开头 DROP 是重置开发环境。
-
「3306 和 8080 是一个服务」
MySQL 3306;Go HTTP 8080。浏览器访问 3306 不对(第 1 篇思考题)。
16. 项目代码对照
| 概念 | 位置 |
|---|---|
| 建库建表 | sql/schema.sql |
| 演示数据 | sql/seed.sql |
| 连接串 | config/config.go → TTMS_DSN 环境变量 |
| 连接池 | db/db.go → Init |
| 表 → struct | models/models.go |
| 读写行 | dao/user.go, dao/order.go, ... |
启动检查清单:
□ MySQL 服务已启动
□ 已执行 schema.sql(有 ttms 库和表)
□ 已执行 seed.sql(有 admin/user 和电影)
□ config DSN 用户名密码与本地 MySQL 一致
□ go run . 看到「MySQL 连接成功」和 8080 启动日志
17. 小结口述稿
MySQL 用库、表、行、列组织数据;我们项目库名是 ttms。
主键 id 自增,用来稳定关联;业务号 order_no 另设 UNIQUE。
金额用 DECIMAL,状态用 TINYINT,和 Go 常量数值对应。
InnoDB 支持事务和行锁;utf8mb4 支持中文和 emoji。
schema 管结构(会 DROP 重建),seed 管演示数据;Go 通过 DSN 连 ttms。
18. 思考题
- 为什么
orders.order_no也 UNIQUE,同时还要有id主键? rating_avg DECIMAL(3,1)最大能表示多少?最小呢?- 若把座位
status存成字符串"locked"而不是 0/1/2,在 SQL 和 Go 里各有什么优缺点? DROP DATABASE IF EXISTS ttms的危险是什么?生产环境能随便跑 schema 吗?- DSN 里
parseTime=true若去掉,Go 里 Scan 到time.Time会怎样?
19. 下一篇预告
《07 · SQL 增删改查与 dao 对应》 ------SELECT/INSERT/UPDATE/DELETE 在本项目里的真实写法,以及 QueryRow、Query、Exec 和 Scan 怎么配合