359 lines
12 KiB
Go
359 lines
12 KiB
Go
/**
|
||
* 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.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"), // 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.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.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)
|
||
}
|
||
}
|
||
|
||
// 步骤10: 启动HTTP服务器
|
||
port := viper.GetString("app.port") // 从配置文件读取端口号
|
||
log.Printf("🚀 服务启动在端口: %s", port)
|
||
r.Run(":" + port) // 启动服务器并监听指定端口
|
||
}
|