This commit is contained in:
2025-12-03 11:00:47 +08:00
parent 29fb7cc9ac
commit a6ea6a2869
30 changed files with 4695 additions and 75 deletions

View File

@@ -1,3 +1,22 @@
/**
* package main
*
* IM系统后端服务主程序
*
* 功能概述:
* 1. 初始化配置、数据库、Redis等基础服务
* 2. 初始化业务服务(认证、用户、联系人、房间、聊天、附件等)
* 3. 启动WebSocket服务器和TURN服务器
* 4. 注册HTTP API路由和中间件
* 5. 启动HTTP服务器
*
* 技术栈:
* - Gin: HTTP Web框架
* - GORM: ORM数据库操作
* - Redis: 缓存和消息队列
* - WebSocket: 实时通信
* - JWT: 身份认证
*/
package main
import (
@@ -8,9 +27,11 @@ import (
"xk-websocket-v2/internal/api"
"xk-websocket-v2/internal/manager"
"xk-websocket-v2/internal/middleware"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/turnserver"
"xk-websocket-v2/internal/utils"
"xk-websocket-v2/internal/ws"
"github.com/gin-gonic/gin"
@@ -21,7 +42,17 @@ import (
"gorm.io/gorm"
)
// ... (initConfig, initDB, initRedis 保持不变) ...
/**
* initConfig
*
* 功能:初始化配置文件读取器
*
* 步骤:
* 1. 设置配置文件名为 "config"
* 2. 设置配置文件类型为 YAML
* 3. 添加配置文件搜索路径(configs目录和当前目录)
* 4. 读取配置文件,如果失败则终止程序
*/
func initConfig() {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
@@ -32,72 +63,227 @@ func initConfig() {
}
}
/**
* initDB
*
* 功能:初始化MySQL数据库连接并执行数据库迁移
*
* 步骤:
* 1. 从配置文件中读取数据库连接字符串(DSN)
* 2. 使用GORM连接MySQL数据库
* 3. 如果连接失败,终止程序
* 4. 执行自动数据库迁移,创建所有表结构
* 5. 为所有表添加中文注释
* 6. 返回数据库连接实例
*
* @returns *gorm.DB 数据库连接实例
*/
func initDB() *gorm.DB {
// 步骤1: 从配置文件读取数据库连接字符串
dsn := viper.GetString("database.dsn")
// 步骤2: 使用GORM连接MySQL数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("❌ 数据库连接失败: %v", err)
}
db.AutoMigrate(&model.ChatMessage{})
// 步骤3: 执行自动数据库迁移,创建所有表结构
err = db.AutoMigrate(
&model.ChatMessage{},
&model.User{},
&model.UserContact{},
&model.ContactGroup{},
&model.ChatRoom{},
&model.FriendRequest{},
&model.VerificationCode{},
&model.Attachment{},
&model.ApiRequestLog{},
&model.LoginLog{},
)
if err != nil {
log.Fatalf("❌ 数据库迁移失败: %v", err)
}
// 步骤4: 为所有表添加中文注释,提高数据库可读性
tableComments := []struct {
table string
comment string
}{
{"chat_messages", "聊天消息表,持久化存储聊天记录,包括文本、多媒体和信令状态"},
{"users", "用户基本信息表"},
{"user_contacts", "用户联系人表,存储好友关系、分组、备注等信息"},
{"contact_groups", "联系人分组表"},
{"chat_rooms", "聊天房间表,支持点对点和群聊"},
{"friend_requests", "好友申请表"},
{"verification_codes", "验证码表"},
{"attachments", "附件表,记录上传的文件信息"},
{"api_request_logs", "接口请求日志表"},
{"login_logs", "登录日志表"},
}
for _, tc := range tableComments {
sql := fmt.Sprintf("ALTER TABLE `%s` COMMENT = '%s'", tc.table, tc.comment)
if err := db.Exec(sql).Error; err != nil {
log.Printf("⚠️ 添加表注释失败 %s: %v", tc.table, err)
}
}
log.Println("✅ 数据库迁移完成")
return db
}
/**
* initRedis
*
* 功能:初始化Redis连接
*
* 步骤:
* 1. 从配置文件读取Redis连接信息(地址、密码、数据库编号)
* 2. 创建Redis客户端实例
* 3. 执行Ping操作测试连接
* 4. 如果连接失败,终止程序
* 5. 返回Redis客户端实例
*
* @returns *redis.Client Redis客户端实例
*/
func initRedis() *redis.Client {
// 步骤1: 从配置文件读取Redis连接信息
rdb := redis.NewClient(&redis.Options{
Addr: viper.GetString("redis.addr"),
Password: viper.GetString("redis.password"),
DB: viper.GetInt("redis.db"),
Addr: viper.GetString("redis.addr"), // Redis服务器地址
Password: viper.GetString("redis.password"), // Redis密码
DB: viper.GetInt("redis.db"), // Redis数据库编号
})
// 步骤2: 执行Ping操作测试连接是否正常
if _, err := rdb.Ping(rdb.Context()).Result(); err != nil {
log.Fatalf("❌ Redis 连接失败: %v", err)
}
return rdb
}
/**
* main
*
* 功能:程序主入口,初始化所有服务并启动HTTP服务器
*
* 执行流程:
* 1. 初始化配置、数据库、Redis
* 2. 启动WebSocket工作池
* 3. 初始化雪花ID生成器
* 4. 初始化所有业务服务
* 5. 启动TURN服务器(用于WebRTC)
* 6. 创建Gin路由引擎
* 7. 注册中间件(响应时间、请求日志、CORS)
* 8. 注册WebSocket路由
* 9. 注册HTTP API路由
* 10. 启动HTTP服务器
*/
func main() {
initConfig()
db := initDB()
rdb := initRedis()
// 步骤1: 初始化基础服务
initConfig() // 读取配置文件
db := initDB() // 连接MySQL数据库
rdb := initRedis() // 连接Redis
// 步骤2: 启动WebSocket工作池,用于处理WebSocket消息
ws.StartWorkerPool()
defer ws.StopWorkerPool()
service.InitChatService(db, rdb)
defer ws.StopWorkerPool() // 程序退出时关闭工作池
// 步骤3: 初始化雪花ID生成器(用于生成全局唯一ID)
// 从配置文件读取数据中心ID和机器ID,如果未配置则使用默认值
datacenterID := viper.GetInt64("snowflake.datacenter_id")
if datacenterID == 0 {
datacenterID = 1 // 默认数据中心ID为1
}
machineID := viper.GetInt64("snowflake.machine_id")
if machineID == 0 {
machineID = 1 // 默认机器ID为1
}
if err := utils.InitSnowflake(datacenterID, machineID); err != nil {
log.Fatalf("❌ 初始化雪花ID生成器失败: %v", err)
}
// 步骤4: 初始化所有业务服务
service.InitChatService(db, rdb) // 聊天服务(消息处理、WebSocket分发)
service.InitAuthService(db, rdb) // 认证服务(登录、注册、验证码)
service.InitUserService(db) // 用户服务(用户信息管理)
service.InitContactService(db) // 联系人服务(好友管理、分组管理)
service.InitRoomService(db) // 房间服务(聊天房间管理)
service.InitAttachmentService(db) // 附件服务(文件上传、管理)
service.InitLoginLogService(db) // 登录日志服务(记录登录历史)
// 步骤5: 启动TURN服务器(用于WebRTC音视频通话)
go turnserver.Start()
// 步骤6: 创建Gin路由引擎
r := gin.Default()
// 允许跨域 (重要:为了前端本地开发)
// 步骤7: 注册中间件(按顺序执行)
// 响应时间统计中间件(必须在最前面,用于记录请求开始时间)
r.Use(middleware.ResponseTimeMiddleware())
// 接口请求日志中间件(记录所有API请求信息)
requestLogMiddleware := middleware.NewRequestLogMiddleware(db)
r.Use(requestLogMiddleware.Handler())
// CORS跨域中间件(允许前端跨域访问,重要:为了前端本地开发)
r.Use(func(c *gin.Context) {
// 设置允许的源(*表示允许所有源)
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
// 设置允许的HTTP方法
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
// 设置允许的请求头
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-User-ID")
// 处理OPTIONS预检请求
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
c.AbortWithStatus(204) // 返回204 No Content
return
}
c.Next()
})
// 静态文件服务(提供附件访问功能)
// 访问路径:/uploads/xxx -> ./uploads/xxx
r.Static("/uploads", "./uploads")
// 步骤8: 注册WebSocket路由
// 路径:GET /ws?user_id=xxx
r.GET("/ws", func(c *gin.Context) {
// 步骤1: 创建WebSocket升级器,允许所有来源连接
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
// 步骤2: 将HTTP连接升级为WebSocket连接
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
return // 升级失败,直接返回
}
// 步骤3: 获取用户ID(从查询参数中获取)
userID := c.Query("user_id")
// 步骤4: 生成唯一的客户端ID(节点ID + 时间戳)
clientID := fmt.Sprintf("%s-%d", viper.GetString("app.node_id"), time.Now().UnixNano())
// 步骤5: 创建客户端对象,包含连接和发送队列
client := &manager.Client{ID: clientID, Conn: conn, SendQueue: make(chan []byte, 256)}
// 步骤6: 注册客户端到管理器
manager.Manager.Register(client)
// 步骤7: 如果提供了用户ID,绑定用户到客户端
if userID != "" {
service.ChatSvc.BindUser(client, userID)
}
// 步骤8: 发送客户端ID给前端
client.SendQueue <- []byte(fmt.Sprintf(`{"clientId": "%s"}`, clientID))
// 步骤9: 循环读取WebSocket消息
for {
_, message, err := conn.ReadMessage()
if err != nil {
// 连接断开,注销客户端
manager.Manager.Unregister(client)
break
}
@@ -107,22 +293,66 @@ func main() {
}
})
// 步骤9: 注册HTTP API路由
apiGroup := r.Group("/api")
{
// 公开接口(不需要认证,任何人都可以访问)
apiGroup.POST("/login", api.LoginHandler)
apiGroup.POST("/register", api.RegisterHandler)
apiGroup.POST("/send-email-code", api.SendEmailCodeHandler)
apiGroup.POST("/send-sms-code", api.SendSmsCodeHandler)
apiGroup.GET("/check-token", api.CheckTokenHandler)
apiGroup.GET("/health", api.HealthHandler)
apiGroup.GET("/ice-servers", api.ICEHandler)
// 消息相关(可选认证,兼容旧代码)
apiGroup.POST("/send", api.SendHandler)
apiGroup.POST("/send-to-user", api.SendToUserHandler)
apiGroup.POST("/bind", api.BindHandler)
apiGroup.GET("/check-user-online", api.CheckUserOnlineHandler)
apiGroup.GET("/messages", api.HistoryHandler)
apiGroup.GET("/messages/sync", api.SyncMessagesHandler)
apiGroup.GET("/health", api.HealthHandler)
apiGroup.GET("/ice-servers", api.ICEHandler)
// 新增:联系人列表
apiGroup.GET("/contacts", api.ContactListHandler)
// 需要认证的接口组(必须携带有效的JWT Token)
authGroup := apiGroup.Group("")
authGroup.Use(middleware.JWTAuthMiddleware()) // 使用JWT认证中间件
{
// 用户管理
authGroup.GET("/user/my-info", api.GetMyInfoHandler)
authGroup.GET("/user/list", api.GetUserListHandler)
authGroup.POST("/user/create", api.CreateUserHandler)
authGroup.POST("/user/update", api.UpdateUserHandler)
authGroup.POST("/user/delete", api.DeleteUserHandler)
// 联系人管理
authGroup.GET("/contacts", api.ContactListHandler)
authGroup.GET("/contacts/search", api.SearchUsersHandler)
authGroup.POST("/contacts/add-friend", api.AddFriendHandler)
authGroup.GET("/contacts/friend-requests", api.GetFriendRequestsHandler)
authGroup.POST("/contacts/accept-request", api.AcceptFriendRequestHandler)
authGroup.POST("/contacts/reject-request", api.RejectFriendRequestHandler)
authGroup.GET("/contacts/groups", api.GetGroupsHandler)
authGroup.POST("/contacts/groups", api.CreateGroupHandler)
authGroup.POST("/contacts/groups/update/:id", api.UpdateGroupHandler)
authGroup.POST("/contacts/groups/delete/:id", api.DeleteGroupHandler)
authGroup.GET("/contacts/:id", api.GetContactDetailHandler)
authGroup.POST("/contacts/update/:id", api.UpdateContactHandler)
authGroup.POST("/contacts/delete/:id", api.DeleteContactHandler)
// 房间管理
authGroup.POST("/rooms", api.CreateRoomHandler)
authGroup.GET("/rooms/:id", api.GetRoomHandler)
// 附件管理
authGroup.POST("/attachments/upload", api.UploadAttachmentHandler)
authGroup.GET("/attachments", api.GetAttachmentsHandler)
authGroup.GET("/attachments/:id", api.GetAttachmentHandler)
authGroup.POST("/attachments/delete/:id", api.DeleteAttachmentHandler)
}
}
port := viper.GetString("app.port")
// 步骤10: 启动HTTP服务器
port := viper.GetString("app.port") // 从配置文件读取端口号
log.Printf("🚀 服务启动在端口: %s", port)
r.Run(":" + port)
r.Run(":" + port) // 启动服务器并监听指定端口
}