鱼皮AI导航
鱼皮AI导航
  • 主页
  • 社区
  • AI工具
  • AI知识库
  • AI提示词
  • MCP
  • Skills
  • AI学习资源
  • AI资讯
  • GitHub开源
登录/注册
站长:程序员鱼皮 老鱼简历 商务合作 增值电信业务经营许可证
2026 鱼皮AI导航 | 沪ICP备19026706号-6
3
3
1

易扣AI (Go + CloudWeGo) 企业级AI智能体项目教程 第2章:后端项目用户模块搭建

绯雾sama
绯雾sama
2026-05-17 11:51
阅读 1241
绯雾sama
2026-05-17 11:51
阅读 1241

本章概述

恭喜各位小伙伴进入第二章!在这一章,我们将学习如何使用 Google 的 go-wire 工具进行依赖注入,搭建一个完整的用户模块。依赖注入是构建大型企业级项目的关键技术,能帮助你写出更清晰、更易测试的代码。

知识点清单

一、go-wire 前提准备

1. 什么是 go-wire?

go-wire 简介:

go-wire 是 Google 开源的一个 Go 语言依赖注入代码生成工具。它通过编译时代码生成的方式,自动处理组件之间的依赖关系。

核心特点:

  • 编译时生成:在编译时生成依赖注入代码,而非运行时反射
  • 类型安全:编译时检查依赖关系,避免运行时错误
  • 性能优异:生成的代码性能接近手写代码
  • 简单易用:通过简单的配置即可自动生成复杂的初始化代码

2. 为什么需要依赖注入?

问题场景:手动管理依赖

假设我们有以下结构:

▼
go
复制代码
// 用户处理器 type UserHandler struct { userService *UserService } // 用户服务 type UserService struct { userRepo *UserRepository } // 用户仓储 type UserRepository struct { db *gorm.DB } // 传统方式:手动创建依赖 func main() { // 1. 创建数据库连接 db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) // 2. 创建仓储 userRepo := &UserRepository{db: db} // 3. 创建服务 userService := &UserService{userRepo: userRepo} // 4. 创建处理器 userHandler := &UserHandler{userService: userService} // 使用处理器... }

存在的问题:

  • 依赖关系复杂:需要手动管理创建顺序
  • 代码重复:每个地方都需要重复创建逻辑
  • 难以测试:无法轻松替换依赖进行测试
  • 维护困难:修改依赖关系需要改动多处代码

解决方案:使用 go-wire

▼
go
复制代码
// wire.go //+build wireinject package main import "github.com/google/wire" func InitializeUserHandler(db *gorm.DB) *UserHandler { wire.Build( NewUserRepository, NewUserService, NewUserHandler, ) return nil } // wire_gen.go (自动生成) func InitializeUserHandler(db *gorm.DB) *UserHandler { userRepository := NewUserRepository(db) userService := NewUserService(userRepository) userHandler := NewUserHandler(userService) return userHandler }

优势:

  • 自动管理依赖:wire 自动分析依赖关系
  • 类型安全:编译时检查依赖是否完整
  • 易于测试:可以轻松替换依赖实现
  • 代码清晰:依赖关系一目了然

3. 安装 go-wire

在项目中添加依赖:

▼
bash
复制代码
# 添加 wire 依赖到 go.mod go get github.com/google/wire

安装 wire 命令行工具:

▼
bash
复制代码
# 安装 wire go get github.com/google/wire/cmd/wire # 验证安装 wire version

4. go-wire 核心概念

Provider(提供者):

Provider 是一个可以产生值的函数,用于创建依赖对象。

▼
go
复制代码
// Provider 示例 // 简单的 Provider func NewDB() (*gorm.DB, error) { return gorm.Open(mysql.Open(dsn), &gorm.Config{}) } // 带依赖的 Provider func NewUserRepository(db *gorm.DB) *UserRepository { return &UserRepository{db: db} } // 带清理函数的 Provider func NewRedis() (*redis.Client, func(), error) { client := redis.NewClient(opts) cleanup := func() { client.Close() } return client, cleanup, nil }

Injector(注入器):

Injector 是一个声明依赖关系的函数,wire 会根据它生成实际的初始化代码。

▼
go
复制代码
//+build wireinject package main import "github.com/google/wire" // Injector 函数声明 func InitializeApp() (*App, func(), error) { wire.Build( NewDB, // 提供 *gorm.DB NewUserRepo, // 需要 *gorm.DB,提供 *UserRepository NewUserService, // 需要 *UserRepository,提供 *UserService NewApp, // 需要 *UserService,提供 *App ) // 以下代码省略 ...... }

WireSet(依赖集合):

WireSet 用于将一组相关的 Provider 组合在一起。

▼
go
复制代码
// 定义 User 模块的 Provider Set var UserSet = wire.NewSet( NewUserRepository, NewUserService, NewUserHandler, ) // 定义数据库的 Provider Set var DBSet = wire.NewSet( NewDB, NewRedis, ) // 在 Injector 中使用 func InitializeApp() (*App, error) { wire.Build( DBSet, // 数据库相关 UserSet, // 用户模块相关 NewApp, ) // 以下代码省略 ...... }

5. go-wire 使用流程

完整的使用流程:

▼
text
复制代码
1. 定义 Provider 函数 ↓ 2. 创建 Injector 函数(添加 wire.Build) ↓ 3. 运行 wire 命令生成代码 ↓ 4. 使用生成的初始化函数

6. go-wire 最佳实践

1. Provider 命名规范

▼
go
复制代码
// ✅ 推荐:使用 New 前缀 func NewUserRepository(db *gorm.DB) *UserRepository func NewUserService(repo *UserRepository) *UserService func NewUserHandler(service *UserService) *UserHandler // ❌ 不推荐:其他命名 func CreateUserRepository(db *gorm.DB) *UserRepository func GetUserRepository(db *gorm.DB) *UserRepository

2. 使用 WireSet 组织依赖

▼
go
复制代码
// internal/repository/wire.go package repository import "github.com/google/wire" var RepositorySet = wire.NewSet( NewUserRepository, NewChatRepository, NewMessageRepository, ) // internal/service/wire.go package service import "github.com/google/wire" var ServiceSet = wire.NewSet( NewUserService, NewChatService, NewMessageService, ) // cmd/wire.go func InitializeApp() (*App, error) { wire.Build( repository.RepositorySet, service.ServiceSet, handler.HandlerSet, NewApp, ) return nil }

3. 接口与实现分离

▼
go
复制代码
// 定义接口 type IUserService interface { GetUser(id int) (*User, error) CreateUser(user *User) error } // 实现接口 type UserService struct { repo IUserRepository } // Provider 返回接口类型 func NewUserService(repo IUserRepository) IUserService { return &UserService{repo: repo} }

4. 错误处理

▼
go
复制代码
// Provider 可以返回 error func NewDB(cfg *config.Config) (*gorm.DB, error) { db, err := gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{}) if err != nil { return nil, fmt.Errorf("failed to connect database: %w", err) } return db, nil } // Injector 也会返回 error func InitializeApp() (*App, error) { wire.Build( NewDB, NewApp, ) return nil }

二、用户模块架构设计

在完成了 go-wire 的学习之后,我们现在开始搭建用户模块。架构设计的第一步是数据库表方案设计,这是整个模块的基础。

1. 数据库设计原则

企业级项目数据库设计原则:

  1. 命名规范

    • 表名:使用小写字母,单词间用下划线分隔(如 user, chat_message)
    • 字段名:使用小写字母,单词间用下划线分隔(如 user_name, create_time)
    • 索引名:使用前缀标识类型(如 uk_ 唯一索引,idx_ 普通索引)
  2. 字段设计规范

    • 主键:使用 bigint 自增,便于分库分表
    • 时间字段:统一使用 datetime 类型
    • 状态字段:使用 tinyint 类型
    • 字符串:根据实际需求选择合适的长度
    • 必须字段:添加 not null 约束
  3. 索引设计原则

    • 为查询频繁的字段添加索引
    • 唯一约束字段添加唯一索引
    • 避免过多索引影响写入性能
    • 联合索引遵循最左前缀原则
  4. 通用字段设计

    • id:主键,bigint 自增
    • create_time:创建时间,自动设置
    • update_time:更新时间,自动更新
    • is_delete:逻辑删除标识,0-未删除,1-已删除

2. 用户表设计方案

用户表结构设计:

▼
sql
复制代码
-- 创建库 create database if not exists yikou_ai; -- 切换库 use yikou_ai; -- 用户表 create table if not exists user ( id bigint auto_increment comment 'id' primary key, user_account varchar(256) not null comment '账号', user_password varchar(512) not null comment '密码', user_name varchar(256) null comment '用户昵称', user_avatar varchar(1024) null comment '用户头像', user_profile varchar(512) null comment '用户简介', user_role varchar(256) default 'user' not null comment '用户角色:user/admin', edit_time datetime default CURRENT_TIMESTAMP not null comment '编辑时间', create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', is_delete tinyint default 0 not null comment '是否删除', UNIQUE KEY uk_userAccount (user_account), INDEX idx_userName (user_name) ) comment '用户' collate = utf8mb4_unicode_ci;

3. 字段详细说明

字段设计详解:

字段名类型约束默认值说明设计理由
idbigintPRIMARY KEY, AUTO_INCREMENT-主键ID使用 bigint 支持大数据量,便于分库分表
user_accountvarchar(256)NOT NULL, UNIQUE-用户账号唯一约束保证账号不重复,256长度足够
user_passwordvarchar(512)NOT NULL-用户密码512长度支持加密后的密码存储
user_namevarchar(256)NULL-用户昵称允许为空,用户可以不设置昵称
user_avatarvarchar(1024)NULL-用户头像URL存储头像图片的URL地址
user_profilevarchar(512)NULL-用户简介用户个人简介,可选字段
user_rolevarchar(256)NOT NULL'user'用户角色默认普通用户,支持扩展更多角色
edit_timedatetimeNOT NULLCURRENT_TIMESTAMP编辑时间记录最后编辑时间
create_timedatetimeNOT NULLCURRENT_TIMESTAMP创建时间自动设置为当前时间
update_timedatetimeNOT NULLCURRENT_TIMESTAMP更新时间自动更新为当前时间
is_deletetinyintNOT NULL0逻辑删除标识0-未删除,1-已删除

4. 索引设计说明

索引设计详解:

▼
sql
复制代码
-- 主键索引(自动创建) PRIMARY KEY (id) -- 唯一索引:保证账号唯一性 UNIQUE KEY uk_userAccount (user_account) -- 普通索引:加速按昵称查询 INDEX idx_userName (user_name)

在数据库可视化界面、本地命令行执行以上创建表的sql语句,或者在GoLand连接到自己的MySQL数据库,新建查询控制台执行sql语句(推荐),因为在后续步骤中,将代码开发集中在ide中可以提高项目的开发效率

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

建议:作为一位优秀的程序员,务必将sql表设计文件保存到项目的目录中,例如 /sql/create_table.sql,便于团队的其他开发者更快地了解整个项目的设计架构

三、开始后端用户模块开发

在完成了 go-wire 的学习和数据库表设计之后,我们现在开始实际开发用户模块。第一步是搭建依赖注入架构,这是整个项目的基础框架。

1. 项目依赖注入架构设计

依赖关系图:

▼
text
复制代码
Config (配置) ↓ Server (服务器) ↓ Router (路由) ↓ Handler (处理器) ↓ Service (业务逻辑) ↓ Db (数据访问)

2. 创建配置初始化 Provider

配置结构体设计:

修改 config/config.go 文件的InitConfig方法:

▼
go
复制代码
// InitConfig 初始化配置 - 这是一个 Provider 函数 // env 参数用于指定配置文件后缀,如 "local" 会读取 config-local.yaml func InitConfig() *Config { // 解析命令行参数 env := flag.String("env", "", "运行环境,如 local, dev, test, prod") flag.Parse() // 获取项目根路径 rootPath, err := GetProjectRootPath() if err != nil { panic(fmt.Errorf("获取项目根路径失败: %w", err)) } // 拼接配置文件目录路径 configPath := filepath.Join(rootPath, "config") // 确定配置文件名称 configName := "config" if *env != "" { configName = fmt.Sprintf("config-%s", *env) } // 设置配置文件名和路径 viper.SetConfigName(configName) // 配置文件名称 viper.SetConfigType("yml") // 配置文件类型 viper.AddConfigPath(configPath) // 配置文件路径 // 读取环境变量 viper.AutomaticEnv() // 读取配置文件 if err := viper.ReadInConfig(); err != nil { panic(fmt.Errorf("读取配置文件失败: %w", err)) } // 解析配置到结构体 cfg := &Config{} if err := viper.Unmarshal(cfg); err != nil { panic(fmt.Errorf("解析配置失败: %w", err)) } return cfg }

3. 创建 Wire 依赖注入配置

创建 wire 目录和文件:

创建 wire/wire.go 文件:

▼
go
复制代码
//go:build wireinject package wire import ( "fmt" "strconv" "github.com/cloudwego/hertz/pkg/app/server" "github.com/google/wire" "github.com/hertz-contrib/swagger" "yikou-ai-go-teach/config" "yikou-ai-go-teach/docs" "yikou-ai-go-teach/internal/router" ) // 配置依赖 Provider Set var configSet = wire.NewSet( config.InitConfig, // 提供 *Config ) // initServer 初始化 Web 服务器 - 这是一个 Provider 函数 func initServer(cfg *config.Config) *server.Hertz { // 动态设置 Swagger 信息 docs.SwaggerInfo.Host = fmt.Sprintf("localhost:%d", cfg.Server.Port) docs.SwaggerInfo.BasePath = cfg.Server.ContextPath // 初始化 swagger 路径 swaggerPath := fmt.Sprintf("http://localhost:%d%s/swagger/doc.json", cfg.Server.Port, cfg.Server.ContextPath) url := swagger.URL(swaggerPath) // 创建 Hertz 服务器 h := server.Default( server.WithHostPorts(":"+strconv.Itoa(cfg.Server.Port)), server.WithBasePath(cfg.Server.ContextPath), ) // 注册路由 router.RegisterRoutes(h, url) return h } // InitializeApp 初始化所有依赖(依赖图) // 这是 Injector 函数,wire 会根据它生成实际的初始化代码 func InitializeApp() (*server.Hertz, error) { panic(wire.Build( initServer, // 需要 *Config,提供 *server.Hertz configSet, // 提供 *Config )) }

4. 生成 Wire 代码

运行 wire 命令:

▼
bash
复制代码
# 在 wire 目录下执行 cd wire wire

生成的 wire/wire_gen.go 文件:

▼
go
复制代码
// Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject package wire import ( "fmt" "strconv" "github.com/cloudwego/hertz/pkg/app/server" "github.com/hertz-contrib/swagger" "yikou-ai-go-teach/config" "yikou-ai-go-teach/docs" "yikou-ai-go-teach/internal/router" ) // InitializeApp is initialized by wire: // InitializeApp = initServer(config.InitConfig) func InitializeApp() (*server.Hertz, error) { configConfig := config.InitConfig() hertz := initServer(configConfig) return hertz, nil }

5. 修改主程序入口

修改 main.go 文件:

▼
go
复制代码
package main import ( "fmt" "yikou-ai-go-teach/wire" ) func main() { // 初始化 Web 服务器(使用 wire 生成的初始化函数) h, err := wire.InitializeApp() if err != nil { panic(fmt.Errorf("依赖注入初始化失败: %w", err)) } // 启动服务器 h.Spin() }

6. 使用 GORM Gen 生成实体结构体

GORM Gen 是 GORM 的代码生成工具,可以根据数据库表结构自动生成 Go 代码,包括:

  • 模型结构体(Model)
  • 查询接口(Query)
  • 基础 CRUD 方法

为什么使用 GORM Gen?

相比于手写 Model,使用 GORM Gen 有以下显著优势:

1)开发效率大幅提升

  • 手写 Model 需要为每个表手动定义结构体、字段标签、方法等,耗时且容易出错
  • GORM Gen 可以根据数据库表结构自动生成所有代码,只需运行一个命令即可完成
  • 对于大型项目,可以节省数小时甚至数天的开发时间

2) 类型安全得到保障

  • 手写 Model 在查询时容易写错字段名,只能在运行时发现错误
  • GORM Gen 生成的代码提供类型安全的查询方法,编译时就能发现错误
  • 例如:q.User.UserName.Eq(name) 比 db.Where("user_name = ?", name) 更安全

3) 维护成本显著降低

  • 手写 Model 在修改表结构后,需要手动更新多处代码,容易遗漏
  • GORM Gen 只需重新运行生成命令,所有相关代码自动更新
  • 避免了因忘记更新代码导致的运行时错误

GORM Gen还有更多功能以及相关介绍可以访问官方文档咨询,接下来我们将会频繁地使用GORM Gen生成数据库表相关的代码文件

创建代码生成脚本:

创建 cmd/gen/main.go 文件:

▼
go
复制代码
package main import ( "gorm.io/gen" "yikou-ai-go-teach/config" "yikou-ai-go-teach/internal/dal" ) func main() { // 1. 初始化配置 initConfig := config.InitConfig() // 2. 初始化数据库连接 db := dal.InitDB(initConfig) // 3. 创建 Gen 生成器 g := gen.NewGenerator(gen.Config{ OutPath: "./internal/dal/query", // 查询代码输出路径 ModelPkgPath: "model", // 模型包路径 Mode: gen.WithoutContext | // 生成的代码不包含 context gen.WithDefaultQuery | // 生成默认查询方法 gen.WithQueryInterface, // 生成查询接口 }) // 4. 使用数据库连接 g.UseDB(db) // 5. 为所有表生成代码 g.ApplyBasic(g.GenerateAllTable()...) // 6. 执行生成 g.Execute() }

生成器配置说明:

配置项值说明
OutPath./internal/dal/query生成的查询代码存放路径
ModelPkgPathmodel生成的模型结构体包名
WithoutContext-生成的代码不包含 context 参数
WithDefaultQuery-生成默认的 CRUD 查询方法
WithQueryInterface-生成查询接口,便于测试

运行代码生成脚本,得到生成好的文件:

在这里插入图片描述

7. 数据库初始化 Provider

在完成了依赖注入架构搭建后,我们需要添加数据库连接的初始化。这是数据访问层的基础。

创建数据库初始化文件:

创建 internal/dal/init.go 文件

▼
go
复制代码
package dal import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "yikou-ai-go-teach/config" ) // InitDB 初始化数据库连接 - 这是一个 Provider 函数 func InitDB(config *config.Config) *gorm.DB { // 检查配置是否为空 if config == nil { panic(fmt.Errorf("配置加载失败")) } // 获取数据库连接字符串 dsn := config.Database.GetDSN() // 连接数据库 db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Info), // 设置日志级别 }) if err != nil { panic(fmt.Errorf("数据库连接失败: %w", err)) } query.SetDefault(db) return db }

更新 wire.go 添加数据库依赖:

▼
go
复制代码
// 数据库依赖 Provider Set var dbSet = wire.NewSet( dal.InitDB, // 提供 *gorm.DB ) // 更新 Injector func InitializeApp() (*server.Hertz, error) { panic(wire.Build( configSet, // 配置 dbSet, // 数据库 initServer, // 服务器 )) }

8. 用户模型开发

在生成了基础的 User 模型后,我们需要对其进行一些定制化修改,并添加必要的工具类。

雪花 ID 生成器

为什么使用雪花 ID?

雪花 ID(Snowflake ID)是 Twitter 开源的分布式 ID 生成算法,具有以下优势:

特性自增 ID雪花 ID
唯一性单机唯一全局唯一
有序性严格递增趋势递增
性能高极高
分布式支持❌✅
信息泄露风险高低

雪花 ID 的组成:

▼
text
复制代码
0 - 41位时间戳 - 10位机器ID - 12位序列号 总共 64 位(int64): - 1 位符号位(始终为 0) - 41 位时间戳(毫秒级,可使用 69 年) - 10 位机器 ID(支持 1024 台机器) - 12 位序列号(每毫秒可生成 4096 个 ID)

创建雪花 ID 生成器:

创建 pkg/snowflake/snowflake.go 文件:

▼
go
复制代码
package snowflake import ( "strconv" "github.com/sony/sonyflake" ) var ( sf *sonyflake.Sonyflake ) // init 初始化雪花 ID 生成器 func init() { sf = sonyflake.NewSonyflake(sonyflake.Settings{ MachineID: func() (uint16, error) { return 1, nil }, // 机器 ID,分布式环境下应动态获取 }) } // GenerateSnowFlakeId 生成雪花 ID(int64) func GenerateSnowFlakeId() (int64, error) { id, err := sf.NextID() if err != nil { return 0, err } return int64(id), nil } // GenerateSnowFlakeIdString 生成雪花 ID(string) func GenerateSnowFlakeIdString() (string, error) { snowFlakeId, err := GenerateSnowFlakeId() if err != nil { return "", err } return strconv.Itoa(int(snowFlakeId)), nil }

使用说明:

▼
go
复制代码
// 生成 int64 类型的 ID id, err := snowflake.GenerateSnowFlakeId() if err != nil { // 处理错误 } fmt.Println("ID:", id) // 输出:ID: 1234567890123456789 // 生成 string 类型的 ID idStr, err := snowflake.GenerateSnowFlakeIdString() if err != nil { // 处理错误 } fmt.Println("ID:", idStr) // 输出:ID: "1234567890123456789"
定义用户角色枚举

创建 pkg/enum/user_role.go 文件:

▼
go
复制代码
package enum // UserRoleEnum 用户角色枚举 type UserRoleEnum string const ( UserRole UserRoleEnum = "user" AdminRole UserRoleEnum = "admin" ) var roleTextMap = map[UserRoleEnum]string{ UserRole: "用户", AdminRole: "管理员", } // GetRoleText 获取角色文本 func (e UserRoleEnum) GetRoleText() string { if text, ok := roleTextMap[e]; ok { return text } return "未知角色" }
修改 User 模型的 ID tag

为什么需要修改 ID 的 JSON 类型?

雪花 ID 是一个 64 位整数(int64),在 JavaScript 中可能会丢失精度:

▼
javascript
复制代码
// JavaScript 中的问题 const id = 1234567890123456789; console.log(id); // 输出:1234567890123456800(精度丢失)

解决方案:将生成的User模型 ID 序列化为字符串

在这里插入图片描述

修改自动生成模型结构体脚本文件

由于 user.gen.go 是自动生成的文件,每次运行自动生成模型的脚本都会被覆盖,所以我们将要对脚本文件的配置做出修改:

修改 cmd/gen/main.go:

▼
go
复制代码
package main import ( "gorm.io/gen" "yikou-ai-go-teach/config" "yikou-ai-go-teach/internal/dal" ) func main() { initConfig := config.InitConfig() db := dal.InitDB(initConfig) g := gen.NewGenerator(gen.Config{ OutPath: "./internal/dal/query", ModelPkgPath: "model", Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, }) g.UseDB(db) // 为所有表统一配置 id 字段的tag处理 g.ApplyBasic(g.GenerateAllTable( gen.FieldJSONTag("id","id,string"), )...) g.Execute() }

9. 正式开发接口

用户注册接口

用户注册是用户模块的核心功能之一,涉及参数校验、密码加密、用户创建等业务逻辑。

Service 接口定义

在 internal/service/user_service.go 中定义用户注册相关接口:

▼
go
复制代码
package service import ( "context" "yikou-ai-go-teach/internal/api" ) // IUserService 用户服务接口 type IUserService interface { // UserRegister 用户注册 UserRegister(ctx context.Context, req *api.YiKouUserRegisterRequest) (int64, error) // GetEncryptPassword 获取加密后的密码 GetEncryptPassword(ctx context.Context, password string) string }
UserService 结构体定义:

创建 internal/logic/user_logic.go 文件:

▼
go
复制代码
package logic import ( "context" "crypto/md5" "encoding/hex" "gorm.io/gorm" "yikou-ai-go-teach/internal/api" "yikou-ai-go-teach/internal/dal/model" "yikou-ai-go-teach/internal/dal/query" "yikou-ai-go-teach/pkg/enum" "yikou-ai-go-teach/pkg/errorutil" "yikou-ai-go-teach/pkg/snowflake" ) // UserService 用户服务实现 type UserService struct { db *gorm.DB // 依赖注入:数据库连接 } // NewUserService 创建用户服务(构造函数) func NewUserService(db *gorm.DB) *UserService { return &UserService{ db: db, } }

密码加密方法:

▼
go
复制代码
// GetEncryptPassword 获取加密后的密码 func (s *UserService) GetEncryptPassword(ctx context.Context, password string) string { h := md5.New() h.Write([]byte("feiwu" + password)) // 加盐(salt = "feiwu") return hex.EncodeToString(h.Sum(nil)) }

用户注册方法:

▼
go
复制代码
// UserRegister 用户注册 func (s *UserService) UserRegister(ctx context.Context, req *api.YiKouUserRegisterRequest) (int64, error) { // 1. 校验参数 if req.UserAccount == "" || req.UserPassword == "" || req.CheckPassword == "" { return 0, errorutil.ParamsError } if len(req.UserAccount) < 4 || len(req.UserAccount) > 12 { return 0, errorutil.ParamsError.WithMessage("用户账号长度必须在4到12之间") } if len(req.UserPassword) < 8 || len(req.UserPassword) > 12 { return 0, errorutil.ParamsError.WithMessage("用户密码长度必须在8到12之间") } if req.UserPassword != req.CheckPassword { return 0, errorutil.ParamsError.WithMessage("两次输入密码不一致") } // 2. 校验用户名是否已被注册 count, _ := query.Use(s.db).User.Where(query.User.UserAccount.Eq(req.UserAccount)).Count() if count > 0 { return 0, errorutil.ParamsError.WithMessage("用户名已被注册") } // 3. 密码加密 encryptPassword := s.GetEncryptPassword(ctx, req.UserPassword) // 4. 生成用户 ID(雪花 ID) userId, err := snowflake.GenerateSnowFlakeId() if err != nil { return 0, err } // 5. 创建用户 newUser := &model.User{ ID: userId, UserAccount: req.UserAccount, UserPassword: encryptPassword, UserName: "无名", // 默认用户名 UserRole: string(enum.UserRole), // 默认角色 } err = query.Use(s.db).User.Create(newUser) if err != nil { return 0, err } return newUser.ID, nil }

注册流程图:

▼
text
复制代码
用户注册请求 ↓ 参数校验(账号、密码长度、确认密码) ↓ 查询账号是否已存在 ↓ 密码加密(MD5 + 盐) ↓ 生成雪花 ID ↓ 创建用户记录 ↓ 返回用户 ID
Handler 接口逻辑

创建 internal/handler/user_handler.go 文件:

▼
go
复制代码
package handler import ( "context" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/protocol/consts" "yikou-ai-go-teach/internal/api" "yikou-ai-go-teach/internal/service" "yikou-ai-go-teach/pkg/response" ) // UserHandler 用户处理器 type UserHandler struct { userService service.IUserService // 依赖注入:用户服务接口 } // NewUserHandler 创建用户处理器(构造函数) func NewUserHandler(userService service.IUserService) *UserHandler { return &UserHandler{ userService: userService, } }

用户注册接口:

▼
go
复制代码
// UserRegister 用户注册 // @Summary 用户注册 // @Description 用户注册 // @Tags 用户模块 // @Accept json // @Produce json // @Param req body api.YiKouUserRegisterRequest true "用户注册请求" // @Success 200 {object} api.YiKouUserRegisterResponse "用户ID" // @Router /user/register [post] func (u *UserHandler) UserRegister(ctx context.Context, c *app.RequestContext) { // 1. 绑定和校验请求参数 req := &api.YiKouUserRegisterRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 2. 调用 Service 层处理业务逻辑 userId, err := u.userService.UserRegister(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 3. 返回成功响应 c.JSON(consts.StatusOK, response.NewSuccessResponse[int64](userId)) }

请求和响应结构体:

创建 internal/api/user.go 文件:

▼
go
复制代码
package api import ( "yikou-ai-go-teach/pkg/response" ) // YiKouUserRegisterRequest 用户注册请求 type YiKouUserRegisterRequest struct { UserAccount string `json:"userAccount"` // 用户账号 UserPassword string `json:"userPassword"` // 用户密码 CheckPassword string `json:"checkPassword"` // 确认密码 } // YiKouUserRegisterResponse 用户注册响应 type YiKouUserRegisterResponse response.BaseResponse[int64]
用户登录相关接口

用户登录模块包含用户登录、获取登录用户信息、退出登录等功能。

Service 接口定义

在 internal/service/user_service.go 中添加用户登录相关接口:

▼
go
复制代码
package service import ( "context" "github.com/cloudwego/hertz/pkg/app" "yikou-ai-go-teach/internal/api" "yikou-ai-go-teach/internal/dal/vo" ) // IUserService 用户服务接口 type IUserService interface { // GetLoginUserVo 获取登录用户信息 GetLoginUserVo(ctx context.Context, c *app.RequestContext) (vo.UserVo, error) // UserLogin 用户登录 UserLogin(ctx context.Context, req *api.YiKouUserLoginRequest, c *app.RequestContext) (vo.UserVo, error) // Logout 退出登录 Logout(ctx context.Context, c *app.RequestContext) error }
Logic 业务逻辑实现

常量定义:

创建 pkg/constants/constants.go 文件:

▼
go
复制代码
package constants // UserLoginState 用户登录状态 Cookie 名称 const UserLoginState = "user_login"

用户登录方法:

▼
go
复制代码
// UserLogin 用户登录 func (s *UserService) UserLogin(ctx context.Context, req *api.YiKouUserLoginRequest, c *app.RequestContext) (vo.UserVo, error) { // 1. 校验参数 if req.UserAccount == "" || req.UserPassword == "" { return vo.UserVo{}, errorutil.ParamsError } // 2. 查询用户是否存在 user, err := query.Use(s.db).User.Where(query.User.UserAccount.Eq(req.UserAccount)).First() if err != nil { return vo.UserVo{}, err } // 3. 校验密码是否正确 encryptPassword := s.GetEncryptPassword(ctx, req.UserPassword) if user.UserPassword != encryptPassword { return vo.UserVo{}, errorutil.ParamsError.WithMessage("密码错误") } // 4. 序列化用户信息 userJson, err := json.Marshal(user) if err != nil { return vo.UserVo{}, err } // 5. 保存用户信息到 Cookie(登录状态) c.SetCookie(constants.UserLoginState, string(userJson), 86400, "/", "", protocol.CookieSameSiteLaxMode, false, true) // 6. 构建脱敏的用户信息 loginUserVo := vo.UserVo{ ID: user.ID, UserAccount: user.UserAccount, UserName: user.UserName, UserAvatar: user.UserAvatar, UserProfile: user.UserProfile, UserRole: user.UserRole, CreateTime: user.CreateTime, UpdateTime: user.UpdateTime, } return loginUserVo, nil }

登录流程图:

▼
text
复制代码
用户登录请求 ↓ 参数校验 ↓ 查询用户信息 ↓ 密码比对 ↓ 生成登录状态(Cookie) ↓ 返回脱敏用户信息

获取登录用户信息:

▼
go
复制代码
// GetLoginUserVo 获取登录用户信息 func (s *UserService) GetLoginUserVo(ctx context.Context, c *app.RequestContext) (vo.UserVo, error) { // 1. 从 Cookie 中获取用户信息 userJson := c.Request.Header.Cookie(constants.UserLoginState) if userJson == nil || string(userJson) == "" { return vo.UserVo{}, errorutil.ParamsError.WithMessage("登录已过期,请重新登录") } // 2. URL 解码 decodeUser, err := url.QueryUnescape(string(userJson)) if err != nil { return vo.UserVo{}, err } // 3. 反序列化用户信息 var user model.User err = json.Unmarshal([]byte(decodeUser), &user) if err != nil { return vo.UserVo{}, err } // 4. 校验用户是否仍然存在 _, err = query.Use(s.db).User.Where(query.User.ID.Eq(user.ID), query.User.IsDelete.Eq(0)).First() if err != nil { return vo.UserVo{}, err } // 5. 构建返回对象 loginUserVo := vo.UserVo{ ID: user.ID, UserAccount: user.UserAccount, UserName: user.UserName, UserAvatar: user.UserAvatar, UserProfile: user.UserProfile, UserRole: user.UserRole, CreateTime: user.CreateTime, UpdateTime: user.UpdateTime, } return loginUserVo, nil }

退出登录:

▼
go
复制代码
// Logout 退出登录 func (s *UserService) Logout(ctx context.Context, c *app.RequestContext) error { // 清除 Cookie c.SetCookie(constants.UserLoginState, "", 0, "/", "", protocol.CookieSameSiteLaxMode, false, true) return nil }
Handler 接口逻辑

用户登录接口:

▼
go
复制代码
// UserLogin 用户登录 // @Summary 用户登录 // @Description 用户登录 // @Tags 用户模块 // @Accept json // @Produce json // @Param req body api.YiKouUserLoginRequest true "用户登录请求" // @Success 200 {object} api.YiKouUserLoginResponse "登录用户信息" // @Router /user/login [post] func (u *UserHandler) UserLogin(ctx context.Context, c *app.RequestContext) { // 1. 绑定和校验请求参数 req := &api.YiKouUserLoginRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 2. 调用 Service 层处理登录逻辑 userVo, err := u.userService.UserLogin(ctx, req, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 3. 返回用户信息(脱敏) c.JSON(consts.StatusOK, response.NewSuccessResponse[vo.UserVo](userVo)) }

获取登录用户信息接口:

▼
go
复制代码
// GetLoginUser 获取登录用户信息 // @Summary 获取登录用户信息 // @Description 获取登录用户信息 // @Tags 用户模块 // @Accept json // @Produce json // @Success 200 {object} api.YiKouUserLoginResponse "登录用户信息" // @Router /user/get/login [get] func (u *UserHandler) GetLoginUser(ctx context.Context, c *app.RequestContext) { // 直接从 Session 或 Token 中获取登录用户信息 userVo, err := u.userService.GetLoginUserVo(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[vo.UserVo](userVo)) }

退出登录接口:

▼
go
复制代码
// Logout 退出登录 // @Summary 退出登录 // @Description 退出登录 // @Tags 用户模块 // @Accept json // @Produce json // @Success 200 {object} bool "退出登录成功" // @Router /user/logout [get] func (u *UserHandler) Logout(ctx context.Context, c *app.RequestContext) { // 清除 Session 或 Token err := u.userService.Logout(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[any](true)) }

请求和响应结构体:

▼
go
复制代码
// YiKouUserLoginRequest 用户登录请求 type YiKouUserLoginRequest struct { UserAccount string `json:"userAccount"` // 用户账号 UserPassword string `json:"userPassword"` // 用户密码 } // YiKouUserLoginResponse 用户登录响应 type YiKouUserLoginResponse response.BaseResponse[vo.UserVo]
用户管理相关接口

用户管理模块包含新增用户、获取用户、删除用户、更新用户、分页查询等CRUD操作。

Service 接口定义

在 internal/service/user_service.go 中添加用户管理相关接口:

▼
go
复制代码
package service import ( "context" "yikou-ai-go-teach/internal/api" "yikou-ai-go-teach/internal/dal/model" "yikou-ai-go-teach/internal/dal/vo" "yikou-ai-go-teach/pkg/response" ) // IUserService 用户服务接口 type IUserService interface { // AddUser 新增用户 AddUser(ctx context.Context, req *api.YiKouUserAddRequest) (int64, error) // GetUser 根据 ID 获取用户 GetUser(ctx context.Context, id int64) (*model.User, error) // GetUserVo 根据 ID 获取用户 VO(脱敏) GetUserVo(ctx context.Context, id int64) (vo.UserVo, error) // DeleteUser 删除用户 DeleteUser(ctx context.Context, id int64) (bool, error) // UpdateUser 更新用户 UpdateUser(ctx context.Context, req *api.YiKouUserUpdateRequest) (bool, error) // ListUserVoByPage 分页查询用户列表 ListUserVoByPage(ctx context.Context, req *api.YiKouUserQueryRequest) (*response.PageResponse[vo.UserVo], error) }

接口方法说明:

方法名参数返回值功能说明
AddUser新增用户请求用户ID新增用户(管理员功能)
GetUser用户ID用户实体根据ID获取用户(含密码)
GetUserVo用户ID用户VO根据ID获取用户(脱敏)
DeleteUser用户ID是否成功删除用户(逻辑删除)
UpdateUser更新请求是否成功更新用户信息
ListUserVoByPage分页请求分页结果分页查询用户列表
Logic 业务逻辑实现

新增用户方法:

▼
go
复制代码
// AddUser 新增用户 func (s *UserService) AddUser(ctx context.Context, req *api.YiKouUserAddRequest) (int64, error) { // 1. 校验参数 if req.UserAccount == "" || req.UserPassword == "" { return 0, errorutil.ParamsError } // 2. 校验用户名是否已被注册 count, _ := query.Use(s.db).User.Where(query.User.UserAccount.Eq(req.UserAccount)).Count() if count > 0 { return 0, errorutil.ParamsError.WithMessage("用户名已被注册") } // 3. 密码加密 encryptPassword := s.GetEncryptPassword(ctx, req.UserPassword) // 4. 生成用户 ID userId, err := snowflake.GenerateSnowFlakeId() if err != nil { return 0, err } // 5. 创建用户 newUser := &model.User{ ID: userId, UserAccount: req.UserAccount, UserPassword: encryptPassword, UserName: req.UserAccount, // 默认使用账号作为昵称 UserAvatar: req.UserAvatar, UserProfile: req.UserProfile, UserRole: req.UserRole, } err = query.Use(s.db).User.Create(newUser) if err != nil { return 0, err } return newUser.ID, nil }

获取用户方法:

▼
go
复制代码
// GetUser 根据 ID 获取用户 func (s *UserService) GetUser(ctx context.Context, id int64) (*model.User, error) { user, err := query.Use(s.db).User.Where(query.User.ID.Eq(id), query.User.IsDelete.Eq(0)).First() if err != nil { return nil, err } return user, nil } // GetUserVo 根据 ID 获取用户 VO(脱敏) func (s *UserService) GetUserVo(ctx context.Context, id int64) (vo.UserVo, error) { user, err := s.GetUser(ctx, id) if err != nil { return vo.UserVo{}, err } return vo.UserVo{ ID: user.ID, UserAccount: user.UserAccount, UserName: user.UserName, UserAvatar: user.UserAvatar, UserProfile: user.UserProfile, UserRole: user.UserRole, CreateTime: user.CreateTime, UpdateTime: user.UpdateTime, }, nil }

删除用户方法:

▼
go
复制代码
// DeleteUser 删除用户 func (s *UserService) DeleteUser(ctx context.Context, id int64) (bool, error) { // 逻辑删除:设置 is_delete = 1 _, err := query.Use(s.db).User.Where(query.User.ID.Eq(id)). Update(query.User.IsDelete, 1) if err != nil { return false, err } return true, nil }

更新用户方法:

▼
go
复制代码
// UpdateUser 更新用户 func (s *UserService) UpdateUser(ctx context.Context, req *api.YiKouUserUpdateRequest) (bool, error) { // 1. 校验用户是否存在 _, err := query.Use(s.db).User.Where(query.User.ID.Eq(int64(req.Id)), query.User.IsDelete.Eq(0)).First() if err != nil { return false, errorutil.ParamsError.WithMessage("用户不存在") } // 2. 更新用户信息 _, err = query.Use(s.db).User.Where(query.User.ID.Eq(int64(req.Id))). Updates(map[string]interface{}{ "user_name": req.UserName, "user_avatar": req.UserAvatar, "user_profile": req.UserProfile, "user_role": req.UserRole, }) if err != nil { return false, err } return true, nil }

分页查询用户列表方法:

▼
go
复制代码
// ListUserVoByPage 分页查询用户列表 func (s *UserService) ListUserVoByPage(ctx context.Context, req *api.YiKouUserQueryRequest) (*response.PageResponse[vo.UserVo], error) { // 1. 构建查询条件 q := query.Use(s.db).User.Where(query.User.IsDelete.Eq(0)) if req.UserAccount != "" { q = q.Where(query.User.UserAccount.Like("%" + req.UserAccount + "%")) } if req.UserName != "" { q = q.Where(query.User.UserName.Like("%" + req.UserName + "%")) } if req.UserRole != "" { q = q.Where(query.User.UserRole.Eq(req.UserRole)) } // 2. 查询总数 total, err := q.Count() if err != nil { return nil, err } // 3. 分页查询 pageNum := req.PageNum pageSize := req.PageSize if pageNum <= 0 { pageNum = 1 } if pageSize <= 0 { pageSize = 10 } users, err := q.Offset((pageNum - 1) * pageSize).Limit(pageSize).Find() if err != nil { return nil, err } // 4. 转换为 VO userVos := make([]vo.UserVo, 0, len(users)) for _, user := range users { userVos = append(userVos, vo.UserVo{ ID: user.ID, UserAccount: user.UserAccount, UserName: user.UserName, UserAvatar: user.UserAvatar, UserProfile: user.UserProfile, UserRole: user.UserRole, CreateTime: user.CreateTime, UpdateTime: user.UpdateTime, }) } // 5. 计算总页数 totalPage := int(total) / pageSize if int(total)%pageSize != 0 { totalPage++ } return &response.PageResponse[vo.UserVo]{ Records: userVos, PageNum: pageNum, PageSize: pageSize, TotalRow: int(total), TotalPage: totalPage, }, nil }
Handler 接口逻辑

UserHandler 结构体定义:

▼
go
复制代码
package handler import ( "context" "strconv" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/protocol/consts" "yikou-ai-go-teach/internal/api" "yikou-ai-go-teach/internal/dal/model" "yikou-ai-go-teach/internal/dal/vo" "yikou-ai-go-teach/internal/service" "yikou-ai-go-teach/pkg/errorutil" "yikou-ai-go-teach/pkg/request" "yikou-ai-go-teach/pkg/response" ) // UserHandler 用户处理器 type UserHandler struct { userService service.IUserService } // NewUserHandler 创建用户处理器 func NewUserHandler(userService service.IUserService) *UserHandler { return &UserHandler{ userService: userService, } }

新增用户接口:

▼
go
复制代码
// AddUser 新增用户 // @Summary 新增用户 // @Description 新增用户 // @Tags 用户模块 // @Accept json // @Produce json // @Param req body api.YiKouUserAddRequest true "新增用户请求" // @Success 200 {object} api.YiKouUserAddResponse "用户ID" // @Router /user/add [post] func (u *UserHandler) AddUser(ctx context.Context, c *app.RequestContext) { req := &api.YiKouUserAddRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } userId, err := u.userService.AddUser(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[int64](userId)) }

获取用户接口:

▼
go
复制代码
// GetUser 根据ID获取用户 // @Summary 根据ID获取用户 // @Description 根据ID获取用户 // @Tags 用户模块 // @Accept json // @Produce json // @Param id query int true "用户ID" // @Success 200 {object} api.YiKouUserGetResponse "用户信息" // @Router /user/get [get] func (u *UserHandler) GetUser(ctx context.Context, c *app.RequestContext) { id := c.Query("id") if id == "" { c.JSON(consts.StatusOK, response.NewErrorResponse[any](errorutil.ParamsError)) return } idInt64, _ := strconv.ParseInt(id, 10, 64) user, err := u.userService.GetUser(ctx, idInt64) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[model.User](*user)) }

删除用户接口:

▼
go
复制代码
// DeleteUser 删除用户 // @Summary 删除用户 // @Description 删除用户 // @Tags 用户模块 // @Accept json // @Produce json // @Param req body request.DeleteRequest true "删除用户请求" // @Success 200 {object} api.YiKouUserDeleteResponse "删除结果" // @Router /user/delete [post] func (u *UserHandler) DeleteUser(ctx context.Context, c *app.RequestContext) { req := &request.DeleteRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } success, err := u.userService.DeleteUser(ctx, int64(req.Id)) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[bool](success)) }

更新用户接口:

▼
go
复制代码
// UpdateUser 更新用户 // @Summary 更新用户 // @Description 更新用户 // @Tags 用户模块 // @Accept json // @Produce json // @Param req body api.YiKouUserUpdateRequest true "更新用户请求" // @Success 200 {object} api.YiKouUserUpdateResponse "更新结果" // @Router /user/update [post] func (u *UserHandler) UpdateUser(ctx context.Context, c *app.RequestContext) { req := &api.YiKouUserUpdateRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } if req.Id == 0 { c.JSON(consts.StatusOK, response.NewErrorResponse[any](errorutil.ParamsError.WithMessage("用户ID不能为空"))) return } success, err := u.userService.UpdateUser(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[bool](success)) }

分页查询接口:

▼
go
复制代码
// ListUserVoByPage 分页获取用户VO列表 // @Summary 分页获取用户VO列表 // @Description 分页获取用户VO列表 // @Tags 用户模块 // @Accept json // @Produce json // @Param req body api.YiKouUserQueryRequest true "分页查询请求" // @Success 200 {object} api.YiKouUserPageVoResponse "分页用户VO列表" // @Router /user/list/page/vo [post] func (u *UserHandler) ListUserVoByPage(ctx context.Context, c *app.RequestContext) { req := &api.YiKouUserQueryRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } pageResponse, err := u.userService.ListUserVoByPage(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[*response.PageResponse[vo.UserVo]](pageResponse)) }

请求和响应结构体:

▼
go
复制代码
// YiKouUserAddRequest 新增用户请求 type YiKouUserAddRequest struct { UserAccount string `json:"userAccount"` // 用户账号 UserPassword string `json:"userPassword"` // 用户密码 UserAvatar string `json:"userAvatar"` // 用户头像 UserProfile string `json:"userProfile"` // 用户简介 UserRole string `json:"userRole"` // 用户角色 } // YiKouUserUpdateRequest 更新用户请求 type YiKouUserUpdateRequest struct { request.DeleteRequest // 嵌入删除请求(包含 Id) UserName string `json:"userName"` // 用户昵称 UserAvatar string `json:"userAvatar"` // 用户头像 UserProfile string `json:"userProfile"` // 用户简介 UserRole string `json:"userRole"` // 用户角色 } // YiKouUserQueryRequest 查询用户请求 type YiKouUserQueryRequest struct { request.PageRequest // 嵌入分页请求 UserAccount string `json:"userAccount"` // 用户账号(模糊查询) UserProfile string `json:"userProfile"` // 用户简介(模糊查询) UserName string `json:"userName"` // 用户昵称(模糊查询) UserRole string `json:"userRole"` // 用户角色(精确查询) }

10. 修改路由配置

路由配置是将 Handler 层的接口方法映射到 HTTP 路由的关键步骤。本节将详细介绍如何配置路由、注册中间件以及组织路由分组。

路由分组的好处:

好处说明
统一前缀所有路由自动添加相同前缀
中间件隔离可以为不同分组应用不同中间件
权限控制便于按权限组织路由
代码清晰路由结构一目了然

路由分组示例:

▼
go
复制代码
// 用户路由分组 userRoute := h.Group("/user") { // 公开接口 userRoute.POST("/register", userHandler.UserRegister) userRoute.POST("/login", userHandler.UserLogin) // 需要登录的接口 userRoute.GET("/get/login", userHandler.GetLoginUser) userRoute.POST("/logout", userHandler.Logout) // 需要管理员权限的接口 userRoute.POST("/add", userHandler.AddUser) userRoute.POST("/delete", userHandler.DeleteUser) }
增加鉴权中间件

鉴权中间件用于验证用户身份和权限,确保只有具备相应权限的用户才能访问特定的接口。

创建鉴权中间件文件:

创建 internal/middleware/auth.go 文件:

▼
go
复制代码
package middleware import ( "context" "encoding/json" "net/url" "github.com/cloudwego/hertz/pkg/app" "gorm.io/gorm" "yikou-ai-go-teach/internal/dal/model" "yikou-ai-go-teach/internal/dal/query" "yikou-ai-go-teach/pkg/constants" "yikou-ai-go-teach/pkg/enum" "yikou-ai-go-teach/pkg/errorutil" ) // AuthMiddleware 鉴权中间件 func AuthMiddleware(roleEnum enum.UserRoleEnum, db *gorm.DB) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { // 1. 校验权限 var decodeUser []byte if roleEnum != "" { // 2. 获取存储在 Cookie 的用户信息 userJson := c.Request.Header.Cookie(constants.UserLoginState) if userJson == nil { c.JSON(200, errorutil.NotLoginError) c.Abort() return } // 3. URL 解码 decodeUserJson, err := url.QueryUnescape(string(userJson)) if err != nil { c.JSON(200, errorutil.NotLoginError) c.Abort() return } decodeUser = []byte(decodeUserJson) } // 4. 解析用户信息 var user model.User err := json.Unmarshal(decodeUser, &user) if err != nil { c.JSON(200, errorutil.SystemError.WithMessage(err.Error())) c.Abort() return } // 5. 校验用户权限等级是否符合要求 dbUser, err := query.Use(db).User.Where(query.User.ID.Eq(user.ID), query.User.IsDelete.Eq(0)).First() if err != nil { c.JSON(200, errorutil.NotAuthError) c.Abort() return } // 6. 如果要求管理员权限,校验用户角色 if roleEnum == enum.AdminRole && enum.UserRoleEnum(dbUser.UserRole) != roleEnum { c.JSON(200, errorutil.NotAuthError) c.Abort() return } // 7. 权限校验通过,继续执行 c.Next(ctx) } }

鉴权流程图:

▼
text
复制代码
请求到达 ↓ 检查是否需要鉴权 ↓ 获取 Cookie 中的用户信息 ↓ URL 解码用户信息 ↓ 反序列化用户对象 ↓ 查询数据库验证用户存在 ↓ 校验用户角色权限 ↓ 权限通过 → 继续执行 Handler 权限失败 → 返回错误响应

在路由中使用鉴权中间件:

▼
go
复制代码
// 用户路由分组 userRoute := h.Group("/user") { // 公开接口(无需鉴权) userRoute.POST("/register", userHandler.UserRegister) userRoute.POST("/login", userHandler.UserLogin) userRoute.GET("/get/vo", userHandler.GetUserVo) // 需要登录的接口(用户权限) userRoute.GET("/get/login", middleware.AuthMiddleware(enum.UserRole, db), userHandler.GetLoginUser, ) userRoute.POST("/logout", middleware.AuthMiddleware(enum.UserRole, db), userHandler.Logout, ) // 需要管理员权限的接口 userRoute.POST("/add", middleware.AuthMiddleware(enum.AdminRole, db), userHandler.AddUser, ) userRoute.GET("/get", middleware.AuthMiddleware(enum.AdminRole, db), userHandler.GetUser, ) userRoute.POST("/delete", middleware.AuthMiddleware(enum.AdminRole, db), userHandler.DeleteUser, ) userRoute.POST("/update", middleware.AuthMiddleware(enum.AdminRole, db), userHandler.UpdateUser, ) userRoute.POST("/list/page/vo", middleware.AuthMiddleware(enum.AdminRole, db), userHandler.ListUserVoByPage, ) }

关键点说明:

  1. 中间件链:可以串联多个中间件,按顺序执行
  2. 权限传递:通过 c.Next(ctx) 继续执行后续中间件或 Handler
  3. 请求中止:通过 c.Abort() 中止请求,不再执行后续处理
  4. 上下文传递:中间件可以在上下文中存储数据,供后续使用

11. 修改依赖注入文件

修改后的 wire/wire.go 文件:

▼
go
复制代码
//go:build wireinject package wire import ( "fmt" "strconv" "github.com/cloudwego/hertz/pkg/app/server" "github.com/google/wire" "github.com/hertz-contrib/swagger" "yikou-ai-go-teach/config" "yikou-ai-go-teach/docs" "yikou-ai-go-teach/internal/dal" "yikou-ai-go-teach/internal/handler" "yikou-ai-go-teach/internal/logic" "yikou-ai-go-teach/internal/router" "yikou-ai-go-teach/internal/service" ) // 配置依赖 var configSet = wire.NewSet( config.InitConfig, ) // 数据库依赖 var dbSet = wire.NewSet( dal.InitDB, ) // Service 依赖 var serviceSet = wire.NewSet( logic.NewUserService, wire.Bind(new(service.IUserService), new(*logic.UserService)), ) // Handler 依赖 var handlerSet = wire.NewSet( handler.NewUserHandler, ) // initServer 初始化 Web 服务器 func initServer(cfg *config.Config, userHandler *handler.UserHandler) *server.Hertz { // 动态设置 Swagger 信息 docs.SwaggerInfo.Host = fmt.Sprintf("localhost:%d", cfg.Server.Port) docs.SwaggerInfo.BasePath = cfg.Server.ContextPath // 初始化 Swagger 路径 swaggerPath := fmt.Sprintf("http://localhost:%d%s/swagger/doc.json", cfg.Server.Port, cfg.Server.ContextPath) url := swagger.URL(swaggerPath) // 创建 Hertz 服务器 h := server.Default( server.WithHostPorts(":"+strconv.Itoa(cfg.Server.Port)), server.WithBasePath(cfg.Server.ContextPath), ) // 注册路由 router.RegisterRoutes(h, url, userHandler) return h } // InitializeApp 初始化所有依赖(依赖图) func InitializeApp() (*server.Hertz, error) { panic(wire.Build( initServer, configSet, dbSet, serviceSet, handlerSet, )) }
增加依赖集合

数据库依赖集合:

▼
go
复制代码
var dbSet = wire.NewSet( dal.InitDB, // 初始化数据库连接 )

Service 依赖集合:

▼
go
复制代码
var serviceSet = wire.NewSet( logic.NewUserService, // 创建 UserService 实例 wire.Bind(new(service.IUserService), new(*logic.UserService)), // 接口绑定 )

这里也许会有小伙伴问道:为什么需要接口绑定?

▼
go
复制代码
// Service 接口定义 type IUserService interface { UserRegister(ctx context.Context, req *api.YiKouUserRegisterRequest) (int64, error) } // Service 实现 type UserService struct { db *gorm.DB } // Handler 依赖接口 type UserHandler struct { userService service.IUserService // 依赖接口,而非实现 }

Wire 接口绑定:

▼
go
复制代码
wire.Bind(new(service.IUserService), new(*logic.UserService))

这行代码告诉 Wire:

  • *logic.UserService 实现了 service.IUserService 接口
  • 当需要 IUserService 时,提供 *UserService 实例

Handler 依赖集合:

▼
go
复制代码
var handlerSet = wire.NewSet( handler.NewUserHandler, // 创建 UserHandler 实例 )

修改Server的Provider方法:

▼
go
复制代码
// initServer 初始化 Web 服务器 func initServer(cfg *config.Config, userHandler *handler.UserHandler, db *gorm.DB) *server.Hertz { // 动态设置 Swagger 信息 docs.SwaggerInfo.Host = fmt.Sprintf("localhost:%d", cfg.Server.Port) docs.SwaggerInfo.BasePath = cfg.Server.ContextPath // 初始化swagger路径 swaggerPath := fmt.Sprintf("http://localhost:%d%s/swagger/doc.json", cfg.Server.Port, cfg.Server.ContextPath) url := swagger.URL(swaggerPath) // 创建 Hertz 服务器 h := server.Default( server.WithHostPorts(":"+strconv.Itoa(cfg.Server.Port)), server.WithBasePath(cfg.Server.ContextPath), ) // 注册路由 router.RegisterRoutes(h, url, db, userHandler) return h }

修改依赖图:

▼
go
复制代码
// InitializeApp 初始化所有依赖(依赖图) func InitializeApp() (*server.Hertz, error) { panic(wire.Build( initServer, configSet, dbSet, serviceSet, handlerSet, )) }
生成依赖注入代码

重新生成代码:

▼
bash
复制代码
cd wire wire

四、测试接口

到这里为止,我们的所有准备工作都基本上做好了,接下来我们执行swagger的生成命令 swag init生成接口文档就可以直接启动服务器。然后访问swagger文档一个一个接口的进行测试,接口测试的具体步骤我就不放给各位小伙伴们了。大家可以锻炼下自己的接口测试能力,按照上面的教程步骤测试接口。

在这里插入图片描述

第二章的内容到此就为止了,在下一章,我会讲解整个项目最关键的部分:AI应用生成模块,相对于前面的教程,下一章的内容绝对包含了目前Go社区中最前沿且最有深度的干货,若想接着关注我的教程实况,请为我的仓库 https://github.com/FeiWuSama/yikou-ai-go 点个Star ⭐

GitHub
AI
AI 零代码应用生成平台
3
1
分享
操作

相关专栏

Go + CloudWeGo 零代码 AI 应用平台

Go + CloudWeGo 零代码 AI 应用平台

Go + CloudWeGo 零代码 AI 应用平台项目实战教程

评论
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
🔥热门工具
朱雀AI检测朱雀AI检测
朱雀AI检测助手朱雀AI检测助手
LovartLovart
老鱼简历老鱼简历
WritePass官网WritePass官网
面多多面多多
CursorCursor
星月写作星月写作
零感AI零感AI
墨得问题墨得问题

目录

🔥 热门文章
1
手把手教你设计一个成熟的 Agent Harness
2500
2
我用 REA 逆向了付费颜色 APP,结果很意外!
570
3
离谱,DeepSeek 崩了 5 小时,大家却喊:梁圣恩情还不完??
420
4
DeepSeek 全新模型发布,性价比最高的一集!
380
5
B 站上 DeepSeek 闹鱼灾了,我发起《大肥鱼清除计划》!
370
6
自己学习ai,直接买cursor 的套餐,还是买gpt 或者claude 的套餐?
360
7
又一个国产版 Codex 神器,也来卷办公了!
330
8
day3-如何写出一个高质量的 Skill(组内分享) 高质量 Skill 不是“一个调得很顺的巨型 Prompt”,而是一个可验证、可回归、可维护的能力包。 先定义“什么算好”,再实现“怎么做好”。评估体系先于 Skill 本身成型。 下面用“故障诊断 Markdown 转 YAML”作为例子,但方法是通用的。 --- 一、先想清楚:Skill 到底是什么 一个 Skill 至少包含四部分: 部分 作用 接口 输入什么、输出什么、边界在哪 流程 分几步做,每步的产物是什么 规则 硬约束、经验规则、失败模式 降级策略 不确定时怎么办,而不是硬编一个看似合理的结果 高质量 Skill 的共同点:职责单一、接口清晰、规则可定位、输出可验证。 反例:把几十条经验全塞进一个主 Prompt,改一条规则要重调整个 Prompt,评估时也不知道是哪条规则退化。 --- 二、核心方法:先建评估,再写 Skill 不要先写转换器再补测试。正确顺序是: 1. 定义输出契约:什么算合法、什么算合格; 2. 建金标准集:一小批人工认可“可放行”的样本; 3. 先写评估 Skill:让它在金标准集上跑出基线; 4. 再迭代生成 Skill:每次修改跑同一套评估,无退化且目标维度提升才算正向。 这就是评估驱动开发:评估是 Skill 的单元测试,金标准集是回归锚点。 --- 三、评估 Skill:三层门禁,逐层递进 前一层不过,后一层不跑。每层留可追溯证据。 第 1 层:确定性硬门禁 用解析器、Schema、校验器完成,不消耗模型成本。 · 输出可解析; · 必填字段齐全,类型/枚举合法; · 引用目标存在,无悬空引用、无不可达节点; · 关键路径至少有一条能到达预期出口。 不通过直接打回。 第 2 层:语义断言 用脚本 + 结构遍历检查“逻辑对不对”,仍然保持确定性。 通用检查项: · 覆盖率:源材料里的关键步骤、分支、条件,输出里是否都有对应? · 完整性:每个判断节点是否都有明确出口?有没有死胡同? · 可达性:每条路径是否都能到达预期终点?有没有无出口循环? · 顺序与依赖:有先后依赖的操作,是否保留了顺序?有没有被拍平成无序列表? 例子:故障诊断转换中,检查“每个判断项是否有是/否/异常出口”“每条路径是否能到根因”。 第 3 层:LLM-as-Judge 只处理解析器表达不了的维度:忠实度、完整性、可执行性。 但 Judge 会漂移,不能单独放行,必须建立在第 1、2 层之上。Judge 的 Prompt、rubric、模型版本都要版本化,换模型必须用金标准集重新校准。 通用 Judge 维度: 维度 问什么 忠实度 输出是否忠实源材料意图?有没有自行发明内容? 完整性 关键步骤、条件、证据是否保留?隐式知识有没有被压成空话? 收敛性 输出是否仍指向原目标,而不是发散? 可执行性 下游不依赖源材料能否直接使用? --- 四、金标准集:回归测试的锚点 不需要一开始就标注几百份。用已认可的样本当种子,扩到 15~30 份,覆盖失败模式而非随机抽样: · 完整正例:典型流程; · 边界/异常分支:异常路径、循环、前置失败回退; · 已知易错模式:实际踩过的坑; · 留出集:不参与调优,只用于最终验收,防过拟合。 金标准不需要完美,只需要当前标准下认可可放行。 回归指标:关键路径用 pass^k(k 次全过),稳定性用 pass@k。 --- 五、工程约束:让批量产出可重放 · 幂等:同输入 + 同 Skill 版本 + 同规则 → 输出一致,否则无法判断变好是 Skill 改了还是随机波动; · 隔离不阻断:单份失败进隔离队列,记录失败层和原因,不阻塞其余; · 版本绑定:规则、rubric、金标准集进 Git;每份报告绑定“源版本 + 生成 Skill 版本 + 评估 Skill 版本 + Judge 模型版本”; · 抽样人工复核:Judge 临界样本 + 易错模式,人工抽检结果反过来校准 rubric。 --- 六、Skill 本身怎么写 · SKILL.md 控制篇幅,写清:输入约定、输出契约、分步流程、硬规则、降级策略; · 经验规则放 references/ 或 rules/,按失败模式分类,不要全塞进主 Prompt; · 不确定时标 needs_review: true,不要硬编看似合理的结果; · 迭代由评估报告驱动:报告告诉你哪层挂了、哪类失败模式,你改对应规则文件,而不是漫无目的调 Prompt。 --- 七、落地顺序(不要反过来) 1. 固化输出契约:Schema + 结构约束; 2. 建金标准集:从已认可的 + 最易出错的样本开始; 3. 先写评估 Skill:跑出基线报告; 4. 再迭代生成 Skill:每次修改对比逐项通过率,无退化且目标提升才算正向; 5. 批量运行 + 隔离回放:失败进队列,结束后人工抽检; 6. 评估 Skill 自身版本化:换 Judge 模型必须重新校准。 --- 八、给组员的三条硬要求 1. 不许凭感觉说“变好了”:每次修改必须附评估报告对比,逐项通过率说话。 2. 不许跳过确定性门禁直接看 Judge 分数:前两层挂了,Judge 分数没有意义。 3. 不许把规则堆进主 Prompt:规则按失败模式分文件,评估报告要能定位到具体文件。 --- 一句话收尾 生成 Skill 决定“能不能做”,评估 Skill 决定“敢不敢用”。 先让评估体系站稳,再让生成器迭代。这样交付的不是一堆“看起来还行”的产出,而是一套可证明、可回归、可交接的 Skill 质量体系。
310
9
AI 越强,被它拒绝的代价就越大 我有时候会想:AI 越来越厉害之后,有些事情,会不会反而越来越难做? 这个想法看起来有点矛盾。AI 明明在降低门槛,让不会写代码的人做出软件,让不会画画的人生成图片,让一个人完成过去需要整支团队才能完成的工作。但今天用 Codex 的时候,AI 拒绝帮我编写 SQL 注入的请求体,而那还是我有源码、在本地跑测试的情况。那么,往后是否只会更难? 我们假设某一天,AI 说:「这个请求违规,我不能帮助你。」到那个时候,你还有多少能力,能绕过它,把事情做完? 这里的「违规」需要打个引号。平台的规则、模型的判断、法律的要求,以及一件事实际有没有造成伤害,彼此之间并不总是画得上等号。一个请求可能确实危险,也可能只是缺少上下文被误判,或者恰好落在某家公司的限制范围里。 无论是哪种情况,随着我们越来越依赖 AI,这句拒绝都可能变得越来越贵。 今天,一个程序员让 AI 帮忙写代码,AI 拒绝了,他大概率还有别的办法:查文档、找开源项目、问同行,或者自己写。麻烦一点,慢一点,但路还在。 可如果十年之后,大部分程序都由 AI 完成,情况就不一样了。 很多人从接触编程的第一天起,就习惯了描述需求、等待生成、运行结果、继续迭代。他们能做出相当复杂的产品,却未必经历过独立处理底层问题的训练。平时,这种能力结构完全够用,效率甚至更高。只有 AI 拒绝的时候,他们才会发现:平常被省掉的那段学习过程,恰好是此刻最需要的东西。 到那时,一次拒绝的成本,可能就从「今晚多花两个小时」,变成「先花半年学会这门技术」。 拿 SQL 注入举个稍显极端的例子:往后,连真正理解这类攻击如何发生、能够独立构造测试的人,会不会也越来越少? 这里也包括那些有正当需求的人。一个开发者想检查自己的旧系统,一个经过授权的安全研究员想复现漏洞,假如 AI 识别到危险模式就拒绝,而使用者又只熟悉「让 AI 帮我检查」,这项原本可以独立完成的工作,就这样卡住了。 但是安全专家不会因此消失,但这类技能可能越来越集中在少数人手里。你仍然可以找专家,只是要付出更多的钱、更多的时间,以及更高的沟通成本。 类似的困境,未必只发生在网络安全领域。 你想从一个停止维护的软件里取回自己的数据,需要解析它的旧文件格式。AI 判断这涉及规避保护措施,拒绝继续。你拥有这些数据,却没有能力把它们读出来。 你买了一台设备,厂商停止提供服务。你想修改程序让它继续运转,过程中需要处理某种访问限制。AI 不愿协助,而你所掌握的维修方式,早已只剩下「把报错发给 AI」。 你想给常用的网站写一个辅助工具,减少重复操作。平台将此归入自动化限制,AI 也据此拒绝。过去会写脚本的人可以自己判断、自己处理;未来的使用者,可能连从哪里开始都不知道。 这些例子有一个共同点:当工具同时承担了「执行工作」和「传授知识」的角色,它的拒绝就不只是挡住了一次任务,也挡住了获得这种能力的机会。 更麻烦的是,这种依赖往往是我们主动选择的,而且每一步都很合理。 既然 AI 几秒钟就能做好,为什么还要花几个月学习?既然公司可以采购成熟的 AI 服务,为什么还要养一支负责基础工作的团队?既然市场上已经很少有人手写这些东西,学校为什么还要花大量时间教? 对个人、公司和学校来说,这些选择都有现实依据。但叠加起来,整个社会保留的独立能力就可能越来越少。等到某项能力突然受到限制,我们才发现,备用方案早已在追求效率的过程中被一点点削掉了。 所以,「AI 越强,做事越便宜」可能只描述了未来的一半。 另一半是:被允许的事情越来越便宜,被拒绝的事情依然需要人工完成,却要面对更少的人才、更高的报价,以及越来越难获得的知识。两边的成本差距,会越拉越大。 有人会说,可以换一个模型,或者用本地模型。这确实是重要的选项。只要工具之间还有充分竞争,开放模型还足够可用,限制就未必能形成真正的门槛。 但备用方案也需要维护。它需要硬件、知识、软件生态,以及一群仍然理解这些系统的人。如果大多数人已经习惯了最方便的商业服务,那么「理论上还有别的办法」和「普通人实际用得起、用得明白」,之间仍然可能隔着很远。 我担心的,正是这段距离。 我不指望 AI 对任何请求都照单全收。对明显会伤害他人的行为设置限制,有充分的理由。但我们也需要关心:限制由谁定义,误判有没有申诉渠道,用户能不能换工具,以及脱离这个工具之后,还剩下多少独立行动的能力。 保留这些能力,平时看起来甚至有点低效,明明可以自动生成,还要理解原理;明明可以一键完成,还要知道手动怎么做。 但这些看似多余的知识,会在工具拒绝、失效、涨价或改变规则的那一刻,成为真正的选择权。 AI 越强,我们可能越需要留意它说「不」的那一刻。 因为那句话的分量,最终取决于:它拒绝之后,我们还能做什么。
310
10
什么是LLM
300