/** * 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 ( "fmt" "log" "net/http" "time" "xk-websocket-v2/internal/api" "xk-websocket-v2/internal/manager" "xk-websocket-v2/internal/mediaserver" "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" "github.com/go-redis/redis/v8" "github.com/gorilla/websocket" "github.com/spf13/viper" "gorm.io/driver/mysql" "gorm.io/gorm" ) /** * initConfig * * 功能:初始化配置文件读取器 * * 步骤: * 1. 设置配置文件名为 "config" * 2. 设置配置文件类型为 YAML * 3. 添加配置文件搜索路径(configs目录和当前目录) * 4. 读取配置文件,如果失败则终止程序 */ func initConfig() { viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath("configs") viper.AddConfigPath(".") if err := viper.ReadInConfig(); err != nil { log.Fatalf("❌ 无法读取配置文件: %v", err) } } /** * 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) } // 步骤3: 执行自动数据库迁移,创建所有表结构 err = db.AutoMigrate( &model.ChatMessage{}, &model.User{}, &model.UserContact{}, &model.ContactGroup{}, &model.ChatRoom{}, &model.ChatConversation{}, &model.FriendRequest{}, &model.VerificationCode{}, &model.Attachment{}, &model.ApiRequestLog{}, &model.LoginLog{}, &model.Moment{}, &model.MomentLike{}, &model.MomentComment{}, &model.MomentNotification{}, &model.MessageReadReceipt{}, &model.UserSetting{}, ) if err != nil { log.Fatalf("❌ 数据库迁移失败: %v", err) } // 步骤4: 为所有表添加中文注释,提高数据库可读性 tableComments := []struct { table string comment string }{ {"chat_messages", "聊天消息表,持久化存储聊天记录,包括文本、多媒体和信令状态"}, {"users", "用户基本信息表"}, {"user_contacts", "用户联系人表,存储好友关系、分组、备注等信息"}, {"chat_conversations", "用户会话表,记录每个用户的最近会话列表和未读信息"}, {"contact_groups", "联系人分组表"}, {"chat_rooms", "聊天房间表,支持点对点和群聊"}, {"friend_requests", "好友申请表"}, {"verification_codes", "验证码表"}, {"attachments", "附件表,记录上传的文件信息"}, {"api_request_logs", "接口请求日志表"}, {"login_logs", "登录日志表"}, {"moments", "朋友圈动态表"}, {"moment_likes", "动态点赞表"}, {"moment_comments", "动态评论表"}, {"moment_notifications", "朋友圈通知表"}, {"message_read_receipts", "消息已读回执表"}, {"user_settings", "用户个性化设置表"}, } 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"), // 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() { // 步骤1: 初始化基础服务 initConfig() // 读取配置文件 db := initDB() // 连接MySQL数据库 rdb := initRedis() // 连接Redis // 初始化 ip2region 离线 IP 库 ip2regionPath := viper.GetString("ip2region.db_path") if ip2regionPath == "" { ip2regionPath = "./ip2region.xdb" } if err := utils.InitIP2Region(ip2regionPath); err != nil { log.Printf("⚠️ ip2region 初始化失败: %v", err) } // 步骤2: 启动WebSocket工作池,用于处理WebSocket消息 ws.StartWorkerPool() 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.InitConversationService(db) // 会话服务(最近聊天列表) service.InitRoomService(db) // 房间服务(聊天房间管理) service.InitAttachmentService(db) // 附件服务(文件上传、管理) service.InitLoginLogService(db) // 登录日志服务(记录登录历史) service.InitMomentService(db) // 朋友圈服务(动态、点赞、评论) service.InitSearchService(db) // 搜索服务(聚合搜索) // 步骤5: 启动TURN服务器(用于WebRTC音视频通话) go turnserver.Start() // 步骤5.5: 启动媒体服务器(WebRTC SFU + RTMP) go mediaserver.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", "*") // 设置允许的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) // 返回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 // 升级失败,直接返回 } // 步骤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, RemoteIP: c.ClientIP(), 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 } // WebSocket 仅处理信令和心跳,不再处理 send_message (改走API) // 但为了兼容,仍保留 PushTask ws.PushTask(client, message) } }) // WebSocket 推流路由(不经过 authGroup,避免中间件干扰 WebSocket 升级) // 路径:GET /api/call/ws-push?stream_id=xxx&user_id=xxx&room_id=xxx&token=xxx r.GET("/api/call/ws-push", api.WSPushHandler) // 步骤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) // 需要认证的接口组(必须携带有效的JWT Token) authGroup := apiGroup.Group("") authGroup.Use(middleware.JWTAuthMiddleware()) // 使用JWT认证中间件 { // 聚合搜索 authGroup.GET("/search", api.GlobalSearchHandler) // 用户管理 authGroup.GET("/user/my-info", api.GetMyInfoHandler) authGroup.GET("/user/list", api.GetUserListHandler) authGroup.GET("/user/:id", api.GetUserByIDHandler) 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("/messages/recall", api.RecallMessageHandler) authGroup.POST("/messages/read-receipts", api.MarkMessagesReadHandler) authGroup.POST("/logout", api.LogoutHandler) // 用户设置 authGroup.GET("/user/settings", api.GetUserSettingsHandler) authGroup.POST("/user/settings", api.UpdateUserSettingsHandler) // 会话管理 authGroup.GET("/conversations/by-room/:room_id", api.GetConversationByRoomHandler) authGroup.GET("/conversations", api.GetConversationListHandler) authGroup.POST("/conversations/reset-unread", api.ResetConversationUnreadHandler) authGroup.POST("/conversations/update", api.UpdateConversationHandler) authGroup.POST("/conversations/delete", api.DeleteConversationHandler) // 房间 / 群聊管理 authGroup.POST("/rooms", api.CreateRoomHandler) authGroup.GET("/rooms/:id", api.GetRoomHandler) // 群聊相关接口 authGroup.POST("/groups", api.CreateChatGroupHandler) authGroup.GET("/groups", api.ListUserGroupsHandler) // 获取用户群聊列表(必须在 /groups/:room_id 之前) authGroup.GET("/groups/:room_id", api.GetGroupInfoHandler) authGroup.GET("/groups/:room_id/members", api.ListGroupMembersHandler) authGroup.POST("/groups/:room_id/members", api.AddGroupMembersHandler) authGroup.POST("/groups/:room_id/members/:user_id/remove", api.RemoveGroupMemberHandler) authGroup.POST("/groups/:room_id/update", api.UpdateGroupInfoHandler) authGroup.POST("/groups/:room_id/members/:user_id/role", api.ChangeMemberRoleHandler) authGroup.POST("/groups/:room_id/quit", api.QuitGroupHandler) authGroup.POST("/groups/:room_id/dissolve", api.DissolveGroupHandler) authGroup.GET("/groups/:room_id/announcement", api.GetGroupAnnouncementHandler) authGroup.POST("/groups/:room_id/announcement", api.UpdateGroupAnnouncementHandler) authGroup.POST("/groups/:room_id/members/:user_id/mute", api.MuteGroupMemberHandler) authGroup.GET("/groups/:room_id/members/:user_id/mute", api.GetMemberMuteStatusHandler) authGroup.GET("/groups/:room_id/settings", api.GetGroupSettingsHandler) authGroup.POST("/groups/:room_id/settings", api.UpdateGroupSettingsHandler) authGroup.GET("/group-notifications", api.GetGroupNotificationsHandler) authGroup.GET("/calls/history", api.GetCallHistoryHandler) // 附件管理 authGroup.POST("/attachments/upload", api.UploadAttachmentHandler) authGroup.GET("/attachments", api.GetAttachmentsHandler) authGroup.GET("/attachments/:id", api.GetAttachmentHandler) authGroup.POST("/attachments/delete/:id", api.DeleteAttachmentHandler) // 朋友圈管理 api.RegisterMomentRoutes(authGroup) // 通话管理(音视频) api.RegisterCallRoutes(authGroup) // 媒体服务器管理 api.RegisterMediaRoutes(authGroup) } } // 步骤10: 启动HTTP服务器 port := viper.GetString("app.port") // 从配置文件读取端口号 log.Printf("🚀 服务启动在端口: %s", port) r.Run(":" + port) // 启动服务器并监听指定端口 }