现在,我们就把上一章建立的“饭店后厨”模型,进行一次真实的“开火预演”。

我将沿用之前的风格,将这些代码片段串成一个完整的故事,并在此过程中进行评估、优化,补充一些关键的“幕后细节”,让整个流程更加清晰和健壮。

1 后厨实战:当一个“用户注册”请求抵达后…

理论讲了这么多,是时候看看我们的“后厨团队”如何处理一份真实的订单了!这份订单就是——“新用户注册”

我们将完整追踪这份订单从“服务员”接单,到“总厨”策划,再到“仓库”存档的全过程,看看我们精心设计的“责任链”是如何优雅运作的。

2 “餐厅开业” - 在 main 中组建团队

在处理任何订单之前,餐厅老板(main函数)必须在开业时把团队组建好,并明确每个人的“汇报关系”。这就是依赖注入的“组装”阶段。

您原来的 main.goinitUser 函数的逻辑非常清晰,但它更适合住在“后勤中心” ioc 包里,而不是待在 main.go 这个“大堂”。我们来优化一下,让 ioc 包名副其实。

不必关注 initUser 函数的所在包位置,这里仅作为展示如何从底层 (dao) 开始,一步步创建实例并向上“注入”依赖,最终组装出一个完整的 UserHandlerinitWebServer 的逻辑也同理。

优化建议:

  • 将这些初始化函数(“团队组建说明书”)全部挪到 ioc 包中。main.go 应该只关心“调用说明书来组建团队”,而不关心“说明书”的具体内容。这让 main.go 保持干净,职责更单一。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
	db := ioc.InitDB()
	udl := initUser(db)
	server := initWebServer(udl)

  server.Run(":8021")
}

func initWebServer(userHdl *web.UserHandler) *gin.Engine {
	server := gin.Default()
	userHdl.RegisterRoutes(server)
	return server
}

func initUser(db *gorm.DB) *web.UserHandler {
	ud := dao.NewUserDAO(db)
	repo := repository.NewUserRepository(ud)
	svc := service.NewUserService(repo)
	u := web.NewUserHandler(svc)
	return u
}

3 “订单”处理流程

现在,团队组建完毕,一位新客人发来 POST /users/signup 请求,订单来了!

第 1 站:web.UserHandler - “服务员”接单

“服务员”是第一个接触到订单(HTTP 请求)的角色。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// internal/web/user.go
package web

import (
	"net/http"

	"einscat.com/user-mgr/internal/domain"
	"einscat.com/user-mgr/internal/service"

	"github.com/gin-gonic/gin"
)

type UserHandler struct {
	// 服务员(Handler)只认识总厨(Service)
	svc *service.UserService
}

func NewUserHandler(svc *service.SERVICE.UserService) *UserHandler {
	return &UserHandler{
		svc: svc,
	}
}

// RegisterRoutes 负责将 Handler 的各个“上菜”方法注册到路由上
func (h *UserHandler) RegisterRoutes(server *gin.Engine) {
	ug := server.Group("/users")
	ug.POST("/signup", h.SignUp)
}

// SignUp 处理注册请求
func (h *UserHandler) SignUp(ctx *gin.Context) {
	type SignUpReq struct {
		NickName    string `json:"nickname"`
		PhoneNumber string `json:"phone_number"`
		Password    string `json:"password"`
	}

	var req SignUpReq
	// 1. 服务员检查菜单(请求体),看客人是否填对了
	if err := ctx.BindJSON(&req); err != nil {
		// 如果填错了,直接告诉客人,不用麻烦后厨
		ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request parameters"})
		return
	}
	
	// 2. 将客人的需求翻译成“后厨通用语言”(domain.User)
	err := h.svc.SignUp(ctx, domain.User{
		NickName:    req.NickName,
		PhoneNumber: req.PhoneNumber,
		Password:    req.Password,
	})
	
	// 3. 根据总厨的处理结果,给客人一个回复
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Signup failed"})
		return
	}
	
	ctx.JSON(http.StatusOK, gin.H{"message": "Signup successful"})
}

解读:

  • UserHandler 通过构造函数 NewUserHandler 接收了 UserService 实例,它只知道有这么个“总厨”可以使唤。
  • SignUp 方法做了三件事:解析并校验前端传来的 JSON -> 调用 service 层处理核心业务 -> 根据 service 的返回结果,给前端一个标准的 HTTP 响应。

第 2 站:service.UserService - “总厨”决策

“总厨”接到了“服务员”递来的标准订单,他需要根据“菜谱”(业务逻辑)来决策。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// internal/service/user.go
package service

import (
	"context"

	"einscat.com/user-mgr/internal/domain"
	"einscat.com/user-mgr/internal/repository"
)

type UserService struct {
	// 总厨(Service)只认识仓库管理员(Repository)
	repo *repository.UserRepository
}

func NewUserService(repo *repository.UserRepository) *UserService {
	return &UserService{
		repo: repo,
	}
}

func (svc *UserService) SignUp(ctx context.Context, u domain.User) error {
	// 菜谱第一步:加密用户密码(重要的业务逻辑!)
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	u.Password = string(hashedPassword)
	
	// 菜谱第二步:通知仓库管理员,把这个新用户信息存档
	return svc.repo.Create(ctx, u)
}

解读:

  • 对于一个简单的注册流程,service 的核心业务逻辑可能就是密码加密参数校验等。
  • 它不关心数据最终存在 MySQL 还是 Redis,它只信任 repository 这个“仓库管理员”,并把 domain.User 这个“后厨通用对象”交给他。
  • 方法中添加 context 是为了保持链路与超时控制。 既然是注册用户,就需要传入用户的注册信息,所以 SignUp 还需要接收下用户的注册信息。 上面提到web下的 handler只能使用service层的东西,但是serviec不能使用 web层的内容,这里需要接收用户信息,就需要定义一个User相关的内容,所以这里在与 web、service同级的地方定义个domain包,在里面定义用户的领域对象,然后在 SignUp 方法中接收一个领域对象。

第 3 站:repository.UserRepository - “仓库管理员”的翻译工作

这是至关重要的一环!“仓库管理员”是连接 业务世界(domain数据世界(dao 的桥梁。

在 Repository 层还完成了 domain.Userdao.User 的转换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// internal/repository/user.go
package repository

import (
	"context"

	"einscat.com/user-mgr/internal/domain"
	"einscat.com/user-mgr/internal/repository/dao"
)

type UserRepository struct {
	// 仓库管理员(Repository)认识档案室的办事员(DAO)
	dao *dao.UserDAO
}

func NewUserRepository(dao *dao.UserDAO) *UserRepository {
	return &UserRepository{
		dao: dao,
	}
}

// Create 负责创建用户
func (r *UserRepository) Create(ctx context.Context, u domain.User) error {
	// 核心职责:将 domain 对象 翻译成 dao 对象
	return r.dao.Insert(ctx, r.toDAO(u))
}

// toDAO 是一个私有辅助函数,负责转换
func (r *UserRepository) toDAO(u domain.User) dao.User {
	return dao.User{
		Id:          u.Id,
		NickName:    u.NickName,
		PhoneNumber: u.PhoneNumber,
		Password:    u.Password,
		// Ctime 在 DAO 层面可以通过 gorm 的 hook 自动填充
	}
}

第 4 站:dao.UserDAO - “档案室办事员”的物理操作

“办事员”是最终的执行者,他只认识和数据库打交道的“档案卡”(dao.User),并使用专业的工具 (gorm) 将其存档。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// internal/repository/dao/user.go
package dao

import (
	"context"
	"time"
	"gorm.io/gorm"
)

type UserDAO struct {
	db *gorm.DB
}

func NewUserDAO(db *gorm.DB) *UserDAO {
	return &UserDAO{
		db: db,
	}
}

// Insert 将一个 dao.User 对象插入数据库
func (dao *UserDAO) Insert(ctx context.Context, u User) error {
	// 使用带有超时的上下文,确保数据库操作不会永远阻塞
	return dao.db.WithContext(ctx).Create(&u).Error
}

// User 结构体严格对应数据库的 users 表结构
// 这是一个 PO (Persistent Object)
type User struct {
	Id          uint64         `gorm:"primaryKey,autoIncrement"`
	NickName    string
	PhoneNumber string
	Password    string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

解读:

  • dao.User 结构体现在使用了 gorm 标签,它和 users 表的字段一一对应。
  • Insert 方法接收的是 dao.User,职责非常纯粹:就是把它存进数据库。

至此,一个“用户注册”请求就走完了它的完整旅程。每层各司其其职,互不干扰,通过依赖注入紧密协作,构成了一套清晰、健壮、易于测试和维护的系统。