Files
nl-im-service/cmd/server/main.go
2025-12-05 13:12:48 +08:00

383 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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/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{},
)
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", "登录日志表"},
}
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
// 步骤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) // 登录日志服务(记录登录历史)
// 步骤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", "*")
// 设置允许的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, 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)
}
})
// 步骤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("/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.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.GET("/group-notifications", api.GetGroupNotificationsHandler)
// 附件管理
authGroup.POST("/attachments/upload", api.UploadAttachmentHandler)
authGroup.GET("/attachments", api.GetAttachmentsHandler)
authGroup.GET("/attachments/:id", api.GetAttachmentHandler)
authGroup.POST("/attachments/delete/:id", api.DeleteAttachmentHandler)
}
}
// 步骤10: 启动HTTP服务器
port := viper.GetString("app.port") // 从配置文件读取端口号
log.Printf("🚀 服务启动在端口: %s", port)
r.Run(":" + port) // 启动服务器并监听指定端口
}