mirror of
https://github.com/2930134478/AI-CS.git
synced 2026-06-15 08:45:41 +08:00
18 KiB
18 KiB
后端学习笔记(Go + Gin + GORM)
一、核心概念理解
1. Gin 框架(Web框架)
类比:就像餐厅的服务员,接收客人的请求,然后告诉厨房(处理函数)做什么菜
gin.Default()= 创建一个 Gin 服务器(就像开一家餐厅)r.POST("/路径", 处理函数)= 注册一个路由(就像在菜单上写:客人点这个菜,叫这个厨师做)r.Run(":8080")= 启动服务器,监听 8080 端口(就像开门营业)
为什么需要?
- 没有框架:需要自己处理 HTTP 请求、解析 JSON、路由等(很麻烦)
- 有了框架:只需要写业务逻辑,框架帮你处理其他事情(简单)
2. GORM(ORM框架)
类比:就像翻译官,帮你把 Go 代码翻译成 SQL 语句
db.Create(&user)= 插入数据(翻译成INSERT INTO users ...)db.Where("username=?", name).First(&user)= 查询数据(翻译成SELECT * FROM users WHERE username=?)db.AutoMigrate(&User{})= 自动创建表(根据结构体自动建表)
为什么需要?
- 不用 GORM:需要手写 SQL 语句(容易出错,不安全)
- 用了 GORM:用 Go 代码操作数据库,自动生成 SQL(安全、简单)
3. 环境变量(.env 文件)
类比:就像餐厅的配置表,记录"数据库地址"、"密码"等敏感信息
godotenv.Load()= 从.env文件读取配置os.Getenv("DB_HOST")= 获取环境变量的值
为什么需要?
- 不写在代码里:密码等信息不会泄露,不同环境可以不同配置
- 写在代码里:密码硬编码,不安全,换环境要改代码
4. 中间件(Middleware)
类比:就像餐厅的"检查站",每个请求都要经过这些检查
r.Use(middleware.Logger())= 记录每个请求的日志(就像记录每个客人点了什么)r.Use(middleware.CORS())= 允许跨域请求(就像允许其他网站访问我们的 API)
为什么需要?
- 每个路由都要写日志、跨域处理等代码(重复)
- 用中间件:统一处理,代码简洁
5. Handler(处理函数)
类比:就像餐厅的厨师,负责处理具体的业务逻辑
func Register(db *gorm.DB) gin.HandlerFunc= 注册用户的处理函数c.ShouldBindJSON(&u)= 把请求的 JSON 数据解析成结构体c.JSON(200, gin.H{"message": "成功"})= 返回 JSON 响应给前端
为什么需要?
- 接收前端请求 → 处理业务逻辑 → 返回结果
6. 初始化默认数据
类比:就像餐厅开业时,自动准备一些基础食材
initDefaultAdmin(db)= 在首次启动时自动创建默认管理员账号- 检查数据库是否已有管理员,如果没有则创建
为什么需要?
- 系统首次启动时,需要有一个管理员账号才能登录
- 避免手动创建管理员账号的麻烦
二、常用英文单词记忆
| 英文 | 中文意思 | 记忆技巧 |
|---|---|---|
| handler | 处理函数 | 记住:handler = 处理者(处理请求的人) |
| middleware | 中间件 | 记住:middle(中间)+ ware(件)= 中间件 |
| router | 路由 | 记住:router = 路由器(把请求路由到对应的处理函数) |
| context | 上下文 | 记住:context = 上下文(包含请求和响应的信息) |
| migrate | 迁移 | 记住:migrate = 迁移(把结构体迁移成数据库表) |
| query | 查询 | 记住:query = 查询(从数据库查数据) |
| bind | 绑定 | 记住:bind = 绑定(把 JSON 绑定到结构体) |
| hash | 哈希 | 记住:hash = 哈希(密码加密) |
| bcrypt | 加密算法 | 记住:bcrypt = 密码加密算法(B + crypt = 加密) |
| env | 环境变量 | 记住:env = environment(环境)的缩写 |
| error | 错误 | 记住:error = 错误(Go 语言中常用) |
| return | 返回 | 记住:return = 返回(返回结果或退出函数) |
三、代码执行流程(就像讲故事)
1. 服务器启动(main.go)
1. 加载 .env 文件(读取数据库配置)
2. 连接数据库(NewDB)
3. 自动创建表(AutoMigrate)
4. 初始化默认管理员(initDefaultAdmin)
5. 创建 Gin 服务器(gin.Default)
6. 注册中间件(Logger、CORS)
7. 注册路由(POST /login, POST /conversation/init, GET /conversations, PUT /conversations/:id/contact 等)
8. 启动服务器(监听 8080 端口)
2. 处理一个请求(比如登录)
1. 前端发送 POST /login 请求
2. Gin 框架接收请求
3. 经过中间件(记录日志、允许跨域)
4. 找到对应的处理函数(Login)
5. 解析 JSON 数据(ShouldBindJSON)
6. 查询用户是否存在(db.Where)
7. 验证密码(bcrypt.CompareHashAndPassword)
8. 返回 JSON 响应(包含 user_id、username、role)
9. 前端收到响应,保存到 localStorage
3. 处理一个请求(比如获取对话列表)
1. 前端发送 GET /conversations 请求
2. Gin 框架接收请求
3. 经过中间件(记录日志、允许跨域)
4. 找到对应的处理函数(ListConversations)
5. 查询数据库(db.Where("status != ?", "closed"))
6. 按更新时间倒序排列(Order("updated_at desc"))
7. 返回 JSON 响应(对话列表)
8. 前端收到响应,显示对话列表
4. 服务器首次启动流程
1. 加载 .env 文件
2. 连接数据库(NewDB)
3. 自动创建表(AutoMigrate)
4. 检查是否有管理员账号(initDefaultAdmin)
5. 如果没有,创建默认管理员(admin/admin123)
6. 创建 Gin 服务器
7. 注册路由和中间件
8. 启动服务器
5. 数据库操作流程
1. 定义结构体(models.User)
2. GORM 自动创建表(AutoMigrate)
3. 查询:db.Where("条件").First(&结果)
4. 插入:db.Create(&数据)
5. 更新:db.Save(&数据)
6. 删除:db.Delete(&数据)
四、为什么这样写?
为什么用 Gin 框架?
- 如果不用:需要自己处理 HTTP 协议、解析请求、路由等(很复杂)
- 用了:只需要写业务逻辑,框架帮你处理其他事情(简单高效)
为什么用 GORM?
- 如果不用:需要手写 SQL,容易出错,有 SQL 注入风险
- 用了:用 Go 代码操作数据库,自动生成安全的 SQL(安全、简单)
为什么用环境变量?
- 如果不用:密码硬编码在代码里,不安全,换环境要改代码
- 用了:配置和代码分离,安全,灵活
为什么用中间件?
- 如果不用:每个路由都要写日志、跨域等代码(重复)
- 用了:统一处理,代码简洁,易维护
为什么用 bcrypt 加密密码?
- 如果不用:密码明文存储,泄露了别人就能登录
- 用了:密码加密存储,即使泄露也无法还原(只能验证)
五、代码结构说明
项目结构
backend/
├── main.go # 入口:环境加载、依赖初始化、路由注册
├── controller/ # HTTP 层:接收请求、参数校验、调用业务服务
├── service/ # 业务层:会话/消息/认证逻辑,使用仓储与 WebSocket
├── service/types.go # 业务层通用请求/返回结构体
├── repository/ # 仓储层:封装 GORM 操作(User/Conversation/Message)
├── router/ # 路由注册集中处(RegisterRoutes)
├── utils/ # 工具函数(请求信息解析、UA 解析等)
├── websocket/ # Hub/Client/Handler:管理长连接与广播
├── infra/ # 基础设施(数据库连接 NewDB 等)
├── middleware/ # 中间件(日志、CORS)
├── models/ # 数据模型(User、Conversation、Message)
└── .env # 环境变量配置
分层架构
main.go
↓ 初始化仓储 / 服务 / 控制器 / WebSocket Hub
router.RegisterRoutes
↓ controller 层(解析请求 → 调用 service)
↓ service 层(编排业务 → 调用 repository、websocket)
↓ repository 层(GORM 操作数据库)
↓ models + 数据库
关键职责拆分:
- controller:只做输入输出转换,避免直接访问数据库
- service:编排业务流程(例如发送消息时刷新会话更新时间、广播 WebSocket)
- repository:封装
FindOpenByVisitorID、ListActive、MarkMessagesRead等数据库操作 - websocket:
hub.go管理连接字典,client.go负责读写协程,handler.go升级 HTTP连接 - utils/request.go:
GetClientIP、ParseUserAgent等通用方法 - service/types.go:
ConversationSummary、MessageItem等跨服务共享结构体
重构之后:新增服务时,只需新增仓储方法 + service + controller,不再在
main.go里堆业务代码。
关键结构体与函数速查
| 名称 | 所在文件 | 作用与要点 |
|---|---|---|
controller.AuthController |
controller/auth_controller.go |
处理登录/退出,依赖 AuthService,统一 JSON 响应格式 |
controller.ConversationController |
controller/conversation_controller.go |
初始化会话、列表、搜索、状态更新、详情查询、更新访客联系信息 |
controller.MessageController |
controller/message_controller.go |
创建消息、拉取消息、批量已读 |
service.AuthService |
service/auth_service.go |
登录校验、账号创建(管理员) |
service.ConversationService |
service/conversation_service.go |
会话编排:初始化系统消息、列表拼装、搜索、状态/联系信息更新 |
service.MessageService |
service/message_service.go |
发送消息、拉取消息、已读逻辑 + WebSocket 广播 |
service.AdminService |
service/admin_service.go |
创建客服/管理员账号 |
repository.ConversationRepository |
repository/conversation_repository.go |
会话数据库操作:按访客查找、批量查询、更新状态/字段等 |
repository.MessageRepository |
repository/message_repository.go |
消息数据库操作:保存、按会话拉取、统计未读、标记已读 |
repository.UserRepository |
repository/user_repository.go |
用户查询/创建 |
router.RegisterRoutes |
router/router.go |
集中注册所有 REST + WebSocket 路由(含 /conversations/:id/contact) |
websocket.Hub / Client |
websocket/hub.go、client.go |
维护对话房间 → 客户端列表,负责广播消息/已读事件 |
utils.GetClientIP / ParseUserAgent |
utils/request.go |
请求上下文工具函数,供访客信息采集、日志使用 |
service/types.go |
- | 定义 ConversationSummary、CreateMessageInput 等跨层 DTO,保持类型统一 |
六、常见代码模式
1. Handler 函数模式
func Register(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 解析请求数据
var u models.User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 2. 业务逻辑处理
// ...
// 3. 返回响应
c.JSON(200, gin.H{"message": "成功"})
}
}
为什么这样写?
- 返回
gin.HandlerFunc:可以传入数据库连接等参数 c *gin.Context:包含请求和响应的信息- 先检查错误,再处理:提前返回,代码清晰
2. 错误处理模式
if err != nil {
// 处理错误
return
}
// 继续执行
为什么这样写?
- Go 语言的习惯:错误优先处理
- 提前返回:避免嵌套,代码清晰
3. 数据库查询模式
var user models.User
if err := db.Where("username=?", name).First(&user).Error; err != nil {
// 没找到
return
}
// 找到了,使用 user
为什么这样写?
First:找到第一条记录,没找到返回错误&user:传入指针,结果存到 user 里- 检查错误:确保数据存在才继续
七、记忆口诀
- Gin:Web框架,处理请求
- GORM:ORM框架,操作数据库
- Handler:处理函数,处理业务逻辑
- Middleware:中间件,统一处理
- Context:上下文,包含请求响应
- Error:错误优先,提前返回
八、常见错误和解决
1. 忘记处理错误
错误:db.Create(&user) 不检查错误
正确:
if err := db.Create(&user).Error; err != nil {
// 处理错误
return
}
2. 环境变量没加载
错误:os.Getenv("DB_HOST") 返回空字符串
解决:
- 检查
.env文件是否存在 - 检查文件编码(不能有 BOM)
- 检查
godotenv.Load()是否执行
3. 数据库连接失败
错误:数据库连接失败
解决:
- 检查 MySQL 是否启动
- 检查
.env配置是否正确 - 检查数据库是否存在
4. 跨域问题
错误:前端请求被阻止 解决:
- 检查是否添加了 CORS 中间件
- 检查中间件配置是否正确
5. 密码加密问题
错误:密码明文存储或验证失败 解决:
- 注册时用
bcrypt.GenerateFromPassword加密 - 登录时用
bcrypt.CompareHashAndPassword验证
6. 默认管理员创建问题
错误:首次启动时没有管理员账号,无法登录 解决:
- 使用
initDefaultAdmin函数在首次启动时自动创建 - 检查是否已存在,避免重复创建
九、调试技巧
1. 查看日志
- 后端启动时看日志输出
- 请求处理时看日志记录
- 错误时看错误信息
2. 使用 Postman 测试
- 不用写前端,直接测试 API
- 可以测试各种情况(成功、失败等)
3. 检查数据库
- 用 MySQL 客户端查看数据
- 确认数据是否正确保存
4. 打印调试信息
log.Printf("调试信息: %v", 变量)
十、学习建议
- 先理解概念:理解框架的作用,不要死记硬背
- 多看代码:看别人的代码,理解写法
- 多写注释:每行代码都写注释,理解为什么这样写
- 小步调试:每次只改一小部分,测试看看效果
- 多看错误:出错时看错误信息,学会查问题
- 画流程图:把代码执行流程画出来,理清思路
十一、WebSocket 实时通信(已实现)
1. WebSocket 的作用
类比:就像对讲机,可以实时双向通信
- HTTP:只能客户端主动请求,服务器被动响应
- WebSocket:客户端和服务器可以随时互相发送消息
为什么需要?
- 新消息需要实时推送给客服,无需手动刷新
- 对话状态更新需要实时同步
- 提升用户体验,感觉更流畅
2. WebSocket 架构
Hub(中心管理器)
├── conversations(对话ID → 客户端列表)
├── register(注册新客户端)
├── unregister(注销客户端)
└── broadcast(广播消息)
Client(客户端连接)
├── hub(所属的 Hub)
├── conn(WebSocket 连接)
├── send(发送消息的通道)
└── conversationID(所属的对话ID)
3. 消息流程
1. 客户端建立 WebSocket 连接
2. 客户端发送对话ID,注册到 Hub
3. Hub 将客户端加入对应对话的客户端列表
4. 当有新消息时,Hub 广播给该对话的所有客户端
5. 客户端收到消息,更新 UI
4. 心跳机制
- 客户端定期发送 ping 消息
- 服务器回复 pong 消息
- 如果长时间没有收到 ping,断开连接
为什么需要?
- 检测连接是否断开
- 保持连接活跃
- 自动清理断开的连接
十二、后端分层架构(2025-11 重构版)
1. 目录结构(核心部分)
backend/
├─ main.go // 程序入口,只负责依赖注入和启动
├─ router/ // 路由注册(把 URL 映射到控制器)
├─ controller/ // 控制器层,处理 HTTP 请求/响应
├─ service/ // 业务层,编排业务逻辑
├─ repository/ // 数据访问层,封装 GORM 操作
├─ utils/ // 工具函数(IP、User-Agent 等)
└─ websocket/ // WebSocket Hub、Client 等
2. 各层职责
- router:统一注册所有路由,中间件分组时也只在这里改。
- controller:只关心“请求 → 调用服务 → 返回响应”,不会出现 SQL 或业务细节。
- service:聚焦业务规则,可以组合多个 repository、触发 WebSocket 推送等。
- repository:集中管理所有数据库读写,便于修改查询逻辑或引入缓存。
- utils:存放通用工具,避免重复代码。
3. 调用顺序
HTTP 请求
↓
router → controller → service → repository
↓
MySQL
(若需要 WebSocket 推送,service 会在写库后调用 hub.BroadcastMessage。)
4. 为什么这样做?
- 代码职责更清晰:想改接口,看 controller;想改业务,看 service;想改 SQL 看 repository。
- 更容易写单元测试:service 不依赖 gin,repository 可以替换成 mock。
- 方便扩展:以后要接入 gRPC/消息队列,只需在 service 层复用已有逻辑。
5. 迁移提示
- 新增接口时先写 service & repository,再写 controller 和 router。
- 记得运行
gofmt(PowerShell 举例):cd backend Get-ChildItem main.go, router, controller, service, repository, utils -Recurse -Filter *.go -File | ForEach-Object { gofmt -w $_.FullName } - main.go 只负责依赖注入(NewRepository → NewService → NewController)。
记住:后端开发就像搭建一个服务系统,框架帮你处理底层细节,你只需要关注业务逻辑!🚀