后端基石:Go 项目初始化与数据库模型设计
本文完整代码可在 InkWords 项目 GitHub 仓库 的
backend目录中找到。我们将从零开始,构建一个现代化 Go 后端项目的基石。
引言:为什么需要良好的项目结构?
想象一下你要建造一座房子。你不会直接把砖块、水泥、木材堆在一起就开始盖,而是先画好设计图 ,规划好地基 ,准备好工具。软件开发也是如此,一个清晰的项目结构和规范的数据模型,就是我们的"设计图"和"地基"。
在 InkWords 这个技术博客生成平台中,后端采用 Go 语言 + Gin 框架,数据库使用 PostgreSQL。今天,我们就来深入剖析这个项目的初始化过程和数据模型设计。
一、项目初始化:Go Modules 与依赖管理
1.1 Go Modules 简介
Go Modules 是 Go 语言的官方依赖管理工具,类似于 Node.js 的 npm 或 Python 的 pip。它通过 go.mod 文件来管理项目的依赖关系。
让我们看看 InkWords 后端的 go.mod 文件:
go
// backend/go.mod
module inkwords-backend // 定义模块名称
go 1.25.4 // 指定 Go 版本
require ( // 项目依赖
github.com/gin-gonic/gin v1.12.0 // Web 框架
github.com/golang-jwt/jwt/v5 v5.3.1 // JWT 认证
github.com/google/uuid v1.6.0 // UUID 生成
github.com/joho/godotenv v1.5.1 // 环境变量加载
github.com/jung-kurt/gofpdf v1.16.2 // PDF 生成
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 // PDF 解析
github.com/nguyenthenguyen/docx v0.0.0-20230621112118-9c8e795a11db // Word 解析
github.com/stretchr/testify v1.11.1 // 测试框架
golang.org/x/crypto v0.49.0 // 加密库
golang.org/x/oauth2 v0.36.0 // OAuth 认证
golang.org/x/sync v0.20.0 // 并发控制
gorm.io/datatypes v1.2.7 // GORM 数据类型
gorm.io/driver/postgres v1.6.0 // PostgreSQL 驱动
gorm.io/gorm v1.31.1 // ORM 框架
)
1.2 如何初始化你的 Go 项目?
如果你是 Go 新手,可以按照以下步骤初始化项目:
bash
# 1. 创建项目目录
mkdir inkwords-backend
cd inkwords-backend
# 2. 初始化 Go Modules
go mod init inkwords-backend
# 3. 添加依赖(以 Gin 为例)
go get github.com/gin-gonic/gin
# 4. 创建基础目录结构
mkdir -p internal/model internal/handler internal/service internal/middleware
mkdir -p pkg/config pkg/database pkg/utils
mkdir -p cmd/api
1.3 项目目录结构解析
backend/
├── cmd/ # 应用程序入口
│ └── api/ # API 服务入口
├── internal/ # 私有应用程序代码
│ ├── model/ # 数据模型(今天重点)
│ ├── handler/ # HTTP 处理器
│ ├── service/ # 业务逻辑层
│ └── middleware/# 中间件
├── pkg/ # 可公开导入的库代码
│ ├── config/ # 配置管理
│ ├── database/ # 数据库连接
│ └── utils/ # 工具函数
├── go.mod # 依赖管理文件
└── go.sum # 依赖校验文件
二、核心数据模型设计
数据模型就像是数据库的"蓝图",它定义了数据的结构和关系。InkWords 有四个核心模型,让我们逐一剖析。
2.1 User 模型:系统的核心访问实体
go
// backend/internal/model/user.go
package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// User 代表系统的核心访问实体
type User struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Username string `gorm:"type:varchar(255);not null" json:"username"`
Email string `gorm:"type:varchar(255);uniqueIndex;not null" json:"email"`
PasswordHash string `gorm:"type:varchar(255)" json:"-"`
GithubID *string `gorm:"type:varchar(255)" json:"github_id"`
WechatOpenID *string `gorm:"type:varchar(255)" json:"wechat_openid"`
AvatarURL string `gorm:"type:varchar(1024)" json:"avatar_url"`
SubscriptionTier int16 `gorm:"type:smallint;default:0" json:"subscription_tier"`
TokensUsed int `gorm:"type:integer;default:0" json:"tokens_used"`
IsEmailVerified bool `gorm:"default:false" json:"is_email_verified"`
FailedLoginAttempts int `gorm:"default:0" json:"-"`
LockedUntil *time.Time `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// BeforeCreate 在插入数据库前自动生成 UUID
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.ID == uuid.Nil {
u.ID = uuid.New()
}
return nil
}
逐行解析:
- UUID 主键 :
gorm:"type:uuid;primaryKey"表示使用 UUID 作为主键,比自增 ID 更安全,支持分布式系统。 - 唯一索引 :
uniqueIndex确保邮箱唯一,防止重复注册。 - JSON 标签 :
json:"-"表示该字段不输出到 JSON(如密码哈希),保护敏感信息。 - 软删除 :
gorm.DeletedAt实现软删除,数据不会真正删除,只是标记删除时间。 - GORM 钩子 :
BeforeCreate方法在创建记录前自动调用,确保每个用户都有唯一的 UUID。
生活化比喻:User 模型就像用户的"身份证",包含了用户的所有基本信息。UUID 就像是身份证号,全球唯一;软删除就像是把用户档案放入"回收站",而不是彻底销毁。
2.2 Blog 模型:博客内容的核心存储
go
// backend/internal/model/blog.go
package model
import (
"time"
"github.com/google/uuid"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// Blog 核心业务表,存储生成的 Markdown 内容及大项目拆解结构
type Blog struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;index:idx_user_parent_chapter;not null" json:"user_id"`
ParentID *uuid.UUID `gorm:"type:uuid;index:idx_user_parent_chapter" json:"parent_id"`
ChapterSort int `gorm:"type:integer;index:idx_user_parent_chapter" json:"chapter_sort"`
Title string `gorm:"type:varchar(255);not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
SourceType string `gorm:"type:varchar(50);not null" json:"source_type"`
Status int16 `gorm:"type:smallint;default:0" json:"status"`
WordCount int `gorm:"type:integer;default:0" json:"word_count"`
TechStacks datatypes.JSON `gorm:"type:jsonb" json:"tech_stacks"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// BeforeCreate 在插入数据库前自动生成 UUID
func (b *Blog) BeforeCreate(tx *gorm.DB) error {
if b.ID == uuid.Nil {
b.ID = uuid.New()
}
return nil
}
关键特性解析:
- 复合索引 :
index:idx_user_parent_chapter创建了一个包含 UserID、ParentID、ChapterSort 的复合索引,优化查询性能。 - JSONB 字段 :
datatypes.JSON和type:jsonb允许存储结构化的 JSON 数据,适合存储技术栈这样的灵活数据。 - 树形结构:通过 ParentID 字段实现博客的章节层级关系,支持大项目拆解。
数据库关系图:
拥有
授权
父子章节
User
uuid
id
PK
string
username
string
email
UK
string
password_hash
string
avatar_url
int16
subscription_tier
bool
is_email_verified
timestamp
created_at
timestamp
updated_at
timestamp
deleted_at
Blog
uuid
id
PK
uuid
user_id
FK
uuid
parent_id
FK
int
chapter_sort
string
title
text
content
string
source_type
int16
status
int
word_count
jsonb
tech_stacks
timestamp
created_at
timestamp
updated_at
timestamp
deleted_at
OAuthToken
2.3 OAuthToken 模型:第三方授权管理
go
// backend/internal/model/oauth_token.go
package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// OAuthToken 第三方授权表,用于管理用户在掘金、CSDN等平台的一键发文授权
type OAuthToken struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"`
PlatformType string `gorm:"type:varchar(50);not null" json:"platform_type"`
AccessToken string `gorm:"type:text;not null" json:"access_token"`
RefreshToken string `gorm:"type:text" json:"refresh_token"`
ExpiresIn int `gorm:"type:integer" json:"expires_in"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// BeforeCreate 在插入数据库前自动生成 UUID
func (o *OAuthToken) BeforeCreate(tx *gorm.DB) error {
if o.ID == uuid.Nil {
o.ID = uuid.New()
}
return nil
}
设计思路:
- 一个用户可以绑定多个第三方平台(GitHub、微信、掘金等)
- 存储 AccessToken 和 RefreshToken 实现长期授权
- 支持 Token 过期自动刷新
2.4 VerificationCode 模型:验证码管理
go
// backend/internal/model/verification_code.go
package model
import (
"time"
"gorm.io/gorm"
)
// VerificationCode 验证码模型
type VerificationCode struct {
ID uint `gorm:"primarykey" json:"id"`
Email string `gorm:"index;not null;type:varchar(255)" json:"email"`
Code string `gorm:"not null;type:varchar(20)" json:"code"`
Type string `gorm:"not null;type:varchar(50)" json:"type"` // "register" or "reset_password"
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
为什么使用自增 ID 而不是 UUID?
验证码是短期数据,通常几分钟后就会过期删除。使用自增 ID(uint)性能更好,存储空间更小,适合这种高频写入、短期存储的场景。
三、GORM 标签详解
GORM 通过结构体标签(Struct Tags)来定义数据库字段的特性:
| 标签 | 说明 | 示例 |
|---|---|---|
primaryKey |
主键字段 | gorm:"primaryKey" |
type |
数据库字段类型 | gorm:"type:uuid" |
not null |
非空约束 | gorm:"not null" |
uniqueIndex |
唯一索引 | gorm:"uniqueIndex" |
index |
普通索引 | gorm:"index" |
default |
默认值 | gorm:"default:0" |
- |
忽略字段 | gorm:"-" |
json |
JSON 序列化标签 | json:"id" |
四、实战:创建并测试模型
4.1 创建数据库连接
go
// pkg/database/database.go
package database
import (
"fmt"
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"inkwords-backend/internal/model"
)
func ConnectDB(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("无法连接数据库: %w", err)
}
log.Println("数据库连接成功")
return db, nil
}
func AutoMigrate(db *gorm.DB) error {
// 自动迁移所有模型
err := db.AutoMigrate(
&model.User{},
&model.Blog{},
&model.OAuthToken{},
&model.VerificationCode{},
)
if err != nil {
return fmt.Errorf("数据库迁移失败: %w", err)
}
log.Println("数据库迁移成功")
return nil
}
4.2 测试数据模型
go
// internal/model/model_test.go
package model_test
import (
"testing"
"time"
"github.com/google/uuid"
"inkwords-backend/internal/model"
"github.com/stretchr/testify/assert"
)
func TestUserModel(t *testing.T) {
// 创建用户实例
user := &model.User{
Username: "testuser",
Email: "test@example.com",
PasswordHash: "hashed_password_123",
}
// 验证 BeforeCreate 钩子
// 在实际测试中,这里会调用 db.Create(user)
// 钩子会自动生成 UUID
assert.NotEqual(t, uuid.Nil, user.ID, "用户 ID 应该被自动生成")
assert.Equal(t, "testuser", user.Username)
assert.Equal(t, "test@example.com", user.Email)
assert.Equal(t, false, user.IsEmailVerified, "新用户邮箱应该未验证")
assert.Equal(t, int16(0), user.SubscriptionTier, "新用户应该是免费层级")
}
func TestBlogModel(t *testing.T) {
userID := uuid.New()
blog := &model.Blog{
UserID: userID,
Title: "Go 语言入门指南",
Content: "这是一篇关于 Go 语言的入门教程...",
SourceType: "markdown",
WordCount: 1500,
}
// 设置技术栈(JSON 格式)
techStacks := `["Go", "Gin", "GORM", "PostgreSQL"]`
// 在实际使用中,我们会使用 json.Marshal
assert.Equal(t, userID, blog.UserID)
assert.Equal(t, "Go 语言入门指南", blog.Title)
assert.Equal(t, 0, blog.ChapterSort, "默认排序应该为 0")
}
五、最佳实践总结
- 使用 UUID 作为主键:适合分布式系统,避免 ID 猜测攻击
- 合理使用索引:在经常查询的字段上创建索引,但不要过度索引
- 软删除设计 :使用
gorm.DeletedAt实现软删除,保留数据历史 - JSONB 字段的妙用:存储灵活的结构化数据,如标签、配置等
- GORM 钩子的使用 :在
BeforeCreate中处理业务逻辑,如生成 UUID - 字段权限控制 :使用
json:"-"隐藏敏感字段
结语
今天我们从零开始,构建了 InkWords 后端项目的基石。我们学习了:
- Go Modules 的依赖管理
- 项目目录结构的最佳实践
- 四大核心数据模型的设计思路
- GORM 标签的详细用法
- 如何编写可测试的模型代码
良好的数据模型设计是系统稳定性的基础。就像建造房子一样,坚实的地基决定了上层建筑的高度和稳定性。
下期预告:数据库连接与自动迁移
在下一篇文章中,我们将深入探讨:
- 如何配置 PostgreSQL 数据库连接
- GORM 自动迁移的原理和实践
- 数据库连接池的优化配置
- 如何编写数据库种子数据
- 多环境数据库配置管理
- 使用 Docker 快速搭建开发环境