# 后端学习笔记(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 函数模式 ```go 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. 错误处理模式 ```go if err != nil { // 处理错误 return } // 继续执行 ``` **为什么这样写?** - Go 语言的习惯:错误优先处理 - 提前返回:避免嵌套,代码清晰 ### 3. 数据库查询模式 ```go var user models.User if err := db.Where("username=?", name).First(&user).Error; err != nil { // 没找到 return } // 找到了,使用 user ``` **为什么这样写?** - `First`:找到第一条记录,没找到返回错误 - `&user`:传入指针,结果存到 user 里 - 检查错误:确保数据存在才继续 ## 七、记忆口诀 1. **Gin**:Web框架,处理请求 2. **GORM**:ORM框架,操作数据库 3. **Handler**:处理函数,处理业务逻辑 4. **Middleware**:中间件,统一处理 5. **Context**:上下文,包含请求响应 6. **Error**:错误优先,提前返回 ## 八、常见错误和解决 ### 1. 忘记处理错误 **错误**:`db.Create(&user)` 不检查错误 **正确**: ```go 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. 打印调试信息 ```go log.Printf("调试信息: %v", 变量) ``` ## 十、学习建议 1. **先理解概念**:理解框架的作用,不要死记硬背 2. **多看代码**:看别人的代码,理解写法 3. **多写注释**:每行代码都写注释,理解为什么这样写 4. **小步调试**:每次只改一小部分,测试看看效果 5. **多看错误**:出错时看错误信息,学会查问题 6. **画流程图**:把代码执行流程画出来,理清思路 ## 十一、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 举例): ```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)。 --- **记住**:后端开发就像搭建一个服务系统,框架帮你处理底层细节,你只需要关注业务逻辑!🚀