diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IM_API_Collection.postman_collection.json b/IM_API_Collection.postman_collection.json
new file mode 100644
index 0000000..0597276
--- /dev/null
+++ b/IM_API_Collection.postman_collection.json
@@ -0,0 +1,791 @@
+{
+ "info": {
+ "_postman_id": "nl-im-api-collection",
+ "name": "IM系统 API 接口集合",
+ "description": "IM系统后端API接口测试集合,包含认证、用户、联系人、房间、消息、附件等所有接口",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "variable": [
+ {
+ "key": "base_url",
+ "value": "http://localhost:12080",
+ "type": "string"
+ },
+ {
+ "key": "token",
+ "value": "",
+ "type": "string"
+ },
+ {
+ "key": "user_id",
+ "value": "10001",
+ "type": "string"
+ }
+ ],
+ "item": [
+ {
+ "name": "认证模块",
+ "item": [
+ {
+ "name": "用户登录",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"account\": \"user1@example.com\",\n \"password\": \"12345678\",\n \"remember\": true\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/login",
+ "host": ["{{base_url}}"],
+ "path": ["api", "login"]
+ }
+ },
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "if (pm.response.code === 200) {",
+ " var jsonData = pm.response.json();",
+ " pm.collectionVariables.set(\"token\", jsonData.token);",
+ " pm.collectionVariables.set(\"user_id\", jsonData.user.id);",
+ "}"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "用户注册",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"newuser@example.com\",\n \"phone\": \"13800138000\",\n \"password\": \"12345678\",\n \"confirm_password\": \"12345678\",\n \"code\": \"123456\",\n \"agree_terms\": true\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/register",
+ "host": ["{{base_url}}"],
+ "path": ["api", "register"]
+ }
+ }
+ },
+ {
+ "name": "发送邮箱验证码",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"target\": \"test@example.com\",\n \"type\": \"email\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/send-email-code",
+ "host": ["{{base_url}}"],
+ "path": ["api", "send-email-code"]
+ }
+ }
+ },
+ {
+ "name": "发送短信验证码",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"target\": \"13800138000\",\n \"type\": \"sms\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/send-sms-code",
+ "host": ["{{base_url}}"],
+ "path": ["api", "send-sms-code"]
+ }
+ }
+ },
+ {
+ "name": "检查Token有效性",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/check-token",
+ "host": ["{{base_url}}"],
+ "path": ["api", "check-token"]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "用户管理",
+ "item": [
+ {
+ "name": "获取当前用户信息",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/user/my-info",
+ "host": ["{{base_url}}"],
+ "path": ["api", "user", "my-info"]
+ }
+ }
+ },
+ {
+ "name": "获取用户列表",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/user/list?page=1&page_size=20",
+ "host": ["{{base_url}}"],
+ "path": ["api", "user", "list"],
+ "query": [
+ {
+ "key": "page",
+ "value": "1"
+ },
+ {
+ "key": "page_size",
+ "value": "20"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "创建用户",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"email\": \"admin@example.com\",\n \"phone\": \"13900139000\",\n \"password\": \"12345678\",\n \"name\": \"管理员\",\n \"avatar\": \"A\",\n \"desc\": \"系统管理员\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/user/create",
+ "host": ["{{base_url}}"],
+ "path": ["api", "user", "create"]
+ }
+ }
+ },
+ {
+ "name": "更新用户信息",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"id\": \"{{user_id}}\",\n \"updates\": {\n \"name\": \"新名称\",\n \"desc\": \"新签名\",\n \"region\": \"北京\"\n }\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/user/update",
+ "host": ["{{base_url}}"],
+ "path": ["api", "user", "update"]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "联系人管理",
+ "item": [
+ {
+ "name": "获取联系人列表",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts"]
+ }
+ }
+ },
+ {
+ "name": "搜索用户",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts/search?keyword=10002",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "search"],
+ "query": [
+ {
+ "key": "keyword",
+ "value": "10002"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "添加好友",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"to_user_id\": \"10002\",\n \"message\": \"你好,我想加你为好友\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/contacts/add-friend",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "add-friend"]
+ }
+ }
+ },
+ {
+ "name": "获取好友申请列表",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts/friend-requests",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "friend-requests"]
+ }
+ }
+ },
+ {
+ "name": "接受好友申请",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"request_id\": 1\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/contacts/accept-request",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "accept-request"]
+ }
+ }
+ },
+ {
+ "name": "拒绝好友申请",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"request_id\": 1\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/contacts/reject-request",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "reject-request"]
+ }
+ }
+ },
+ {
+ "name": "获取分组列表",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts/groups",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "groups"]
+ }
+ }
+ },
+ {
+ "name": "创建分组",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"group_name\": \"家人\"\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/contacts/groups",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "groups"]
+ }
+ }
+ },
+ {
+ "name": "更新分组",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"group_name\": \"朋友\",\n \"sort_order\": 1\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/contacts/groups/update/1",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "groups", "update", "1"]
+ }
+ }
+ },
+ {
+ "name": "删除分组",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts/groups/delete/1",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "groups", "delete", "1"]
+ }
+ }
+ },
+ {
+ "name": "获取好友详情",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts/10002",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "10002"]
+ }
+ }
+ },
+ {
+ "name": "更新好友信息",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"remark_name\": \"备注名称\",\n \"group_id\": 1,\n \"is_top\": true,\n \"is_muted\": false\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/contacts/update/10002",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "update", "10002"]
+ }
+ }
+ },
+ {
+ "name": "删除好友",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/contacts/delete/10002",
+ "host": ["{{base_url}}"],
+ "path": ["api", "contacts", "delete", "10002"]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "房间管理",
+ "item": [
+ {
+ "name": "创建房间",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"room_type\": \"p2p\",\n \"members\": [\"{{user_id}}\", \"10002\"]\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/rooms",
+ "host": ["{{base_url}}"],
+ "path": ["api", "rooms"]
+ }
+ }
+ },
+ {
+ "name": "获取房间信息",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/rooms/10001_10002",
+ "host": ["{{base_url}}"],
+ "path": ["api", "rooms", "10001_10002"]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "消息管理",
+ "item": [
+ {
+ "name": "发送消息",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "X-User-ID",
+ "value": "{{user_id}}"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"receiver_user_id\": \"10002\",\n \"room_id\": \"10001_10002\",\n \"message_type\": 0,\n \"content\": \"这是一条测试消息\",\n \"duration\": 0\n}"
+ },
+ "url": {
+ "raw": "{{base_url}}/api/send",
+ "host": ["{{base_url}}"],
+ "path": ["api", "send"]
+ }
+ }
+ },
+ {
+ "name": "获取历史消息",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/messages?room_id=10001_10002&page=1&page_size=50",
+ "host": ["{{base_url}}"],
+ "path": ["api", "messages"],
+ "query": [
+ {
+ "key": "room_id",
+ "value": "10001_10002"
+ },
+ {
+ "key": "page",
+ "value": "1"
+ },
+ {
+ "key": "page_size",
+ "value": "50"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "附件管理",
+ "item": [
+ {
+ "name": "上传附件",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "body": {
+ "mode": "formdata",
+ "formdata": [
+ {
+ "key": "file",
+ "type": "file",
+ "src": []
+ },
+ {
+ "key": "type",
+ "value": "image",
+ "type": "text"
+ }
+ ]
+ },
+ "url": {
+ "raw": "{{base_url}}/api/attachments/upload",
+ "host": ["{{base_url}}"],
+ "path": ["api", "attachments", "upload"]
+ }
+ }
+ },
+ {
+ "name": "获取附件信息",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/attachments/1",
+ "host": ["{{base_url}}"],
+ "path": ["api", "attachments", "1"]
+ }
+ }
+ },
+ {
+ "name": "获取附件列表",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/attachments?type=image&page=1&page_size=20",
+ "host": ["{{base_url}}"],
+ "path": ["api", "attachments"],
+ "query": [
+ {
+ "key": "type",
+ "value": "image"
+ },
+ {
+ "key": "page",
+ "value": "1"
+ },
+ {
+ "key": "page_size",
+ "value": "20"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "删除附件",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{token}}"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/attachments/delete/1",
+ "host": ["{{base_url}}"],
+ "path": ["api", "attachments", "delete", "1"]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "系统接口",
+ "item": [
+ {
+ "name": "健康检查",
+ "request": {
+ "method": "GET",
+ "url": {
+ "raw": "{{base_url}}/api/health",
+ "host": ["{{base_url}}"],
+ "path": ["api", "health"]
+ }
+ }
+ },
+ {
+ "name": "获取ICE服务器配置",
+ "request": {
+ "method": "GET",
+ "url": {
+ "raw": "{{base_url}}/api/ice-servers?user_id={{user_id}}",
+ "host": ["{{base_url}}"],
+ "path": ["api", "ice-servers"],
+ "query": [
+ {
+ "key": "user_id",
+ "value": "{{user_id}}"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "检查用户在线状态",
+ "request": {
+ "method": "GET",
+ "url": {
+ "raw": "{{base_url}}/api/check-user-online?user_id=10002",
+ "host": ["{{base_url}}"],
+ "path": ["api", "check-user-online"],
+ "query": [
+ {
+ "key": "user_id",
+ "value": "10002"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
+
diff --git a/cmd/server/main.go b/cmd/server/main.go
index e2b1481..463ed41 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -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) // 启动服务器并监听指定端口
}
diff --git a/configs/config.yaml b/configs/config.yaml
index 685cd8f..4407dae 100644
--- a/configs/config.yaml
+++ b/configs/config.yaml
@@ -16,7 +16,8 @@ app:
database:
# 数据库连接字符串 (DSN)
# 格式: user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
- dsn: "root:root@tcp(127.0.0.1:3306)/nl_im?charset=utf8mb4&parseTime=True&loc=Local"
+# dsn: "root:root@tcp(127.0.0.1:3306)/nl_im?charset=utf8mb4&parseTime=True&loc=Local"
+ dsn: "root:mysql_PKC65h@tcp(101.43.12.11:3306)/nl_im_plus?charset=utf8mb4&parseTime=True&loc=Local"
# 连接池最大空闲连接数
max_idle_conns: 10
# 连接池最大打开连接数
@@ -26,11 +27,18 @@ database:
# Redis 配置 (用于缓存和集群消息广播)
# ==========================================
redis:
- addr: "127.0.0.1:6379"
- password: "redis_pBjaRs"
+ addr: "101.43.12.11:6379"
+ password: "redis_xeePNa"
# 数据库索引 (0-15)
db: 0
+# ==========================================
+# JWT 配置
+# ==========================================
+jwt:
+ # JWT签名密钥(生产环境应使用强随机密钥)
+ secret: "xk-websocket-jwt-secret-key-2025-change-in-production"
+
# ==========================================
# TURN/STUN 服务器配置 (WebRTC 中继)
# ==========================================
@@ -38,7 +46,7 @@ turn:
# 是否启用内置 TURN 服务
enabled: true
# 服务器公网 IP (重要:客户端必须能访问该IP)
- public_ip: "127.0.0.1"
+ public_ip: "101.43.12.11"
# TURN 服务监听端口 (UDP & TCP)
listen_port: 3478
# 认证领域名称
diff --git a/go.mod b/go.mod
index 30b6d67..44fcc14 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,12 @@ go 1.24.1
require (
github.com/gin-gonic/gin v1.11.0
github.com/go-redis/redis/v8 v8.11.5
+ github.com/golang-jwt/jwt/v5 v5.3.0
github.com/gorilla/websocket v1.5.3
github.com/panjf2000/ants/v2 v2.11.3
github.com/pion/turn/v2 v2.1.6
github.com/spf13/viper v1.21.0
+ golang.org/x/crypto v0.45.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
)
@@ -59,7 +61,6 @@ require (
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
- golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
diff --git a/go.sum b/go.sum
index 0d20b6c..46ea8fd 100644
--- a/go.sum
+++ b/go.sum
@@ -43,6 +43,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/internal/api/attachment_handler.go b/internal/api/attachment_handler.go
new file mode 100644
index 0000000..568951e
--- /dev/null
+++ b/internal/api/attachment_handler.go
@@ -0,0 +1,153 @@
+/**
+ * package api
+ * 作用:附件管理相关API处理器
+ */
+package api
+
+import (
+ "strconv"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/service"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * UploadAttachmentHandler
+ * 功能:上传附件
+ * 路径:POST /api/attachments/upload
+ */
+func UploadAttachmentHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ // 获取文件
+ file, err := c.FormFile("file")
+ if err != nil {
+ utils.BadRequest(c, "文件上传失败: "+err.Error())
+ return
+ }
+
+ // 获取文件类型
+ fileType := c.PostForm("type")
+ if fileType == "" {
+ // 根据文件扩展名推断类型
+ ext := file.Filename[len(file.Filename)-4:]
+ if ext == ".jpg" || ext == ".png" || ext == ".gif" || ext == "webp" || ext == "jpeg" {
+ fileType = "image"
+ } else {
+ fileType = "video"
+ }
+ }
+
+ // 验证文件类型
+ if fileType != "image" && fileType != "video" {
+ utils.BadRequest(c, "文件类型必须是image或video")
+ return
+ }
+
+ // 验证文件大小
+ if fileType == "image" && file.Size > model.MaxImageSize {
+ utils.BadRequest(c, "图片大小不能超过10MB")
+ return
+ }
+ if fileType == "video" && file.Size > model.MaxVideoSize {
+ utils.BadRequest(c, "视频大小不能超过500MB")
+ return
+ }
+
+ // 打开文件
+ src, err := file.Open()
+ if err != nil {
+ utils.BadRequest(c, "打开文件失败: "+err.Error())
+ return
+ }
+ defer src.Close()
+
+ // 上传文件
+ attachment, err := service.AttachmentSvc.UploadFile(
+ userID.(string),
+ file.Filename,
+ fileType,
+ file.Size,
+ src,
+ )
+ if err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ utils.SuccessWithData(c, attachment, "上传成功")
+}
+
+/**
+ * GetAttachmentHandler
+ * 功能:获取附件信息
+ * 路径:GET /api/attachments/:id
+ */
+func GetAttachmentHandler(c *gin.Context) {
+ attachmentID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
+
+ attachment, err := service.AttachmentSvc.GetAttachment(uint(attachmentID))
+ if err != nil {
+ utils.NotFound(c, "附件不存在")
+ return
+ }
+
+ utils.SuccessWithData(c, attachment, "获取成功")
+}
+
+/**
+ * DeleteAttachmentHandler
+ * 功能:删除附件(仅上传者可删除)
+ * 路径:DELETE /api/attachments/:id
+ */
+func DeleteAttachmentHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ attachmentID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
+
+ if err := service.AttachmentSvc.DeleteAttachment(uint(attachmentID), userID.(string)); err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ utils.Success(c, "附件已删除")
+}
+
+/**
+ * GetAttachmentsHandler
+ * 功能:获取附件列表
+ * 路径:GET /api/attachments
+ */
+func GetAttachmentsHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ fileType := c.Query("type")
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
+
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 100 {
+ pageSize = 20
+ }
+
+ attachments, total, err := service.AttachmentSvc.GetUserAttachments(userID.(string), fileType, page, pageSize)
+ if err != nil {
+ utils.InternalError(c, "查询失败")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if attachments == nil {
+ attachments = []model.Attachment{}
+ }
+
+ utils.SuccessWithData(c, gin.H{
+ "data": attachments,
+ "total": total,
+ "page": page,
+ "size": pageSize,
+ }, "获取成功")
+}
+
diff --git a/internal/api/auth_handler.go b/internal/api/auth_handler.go
new file mode 100644
index 0000000..1652d90
--- /dev/null
+++ b/internal/api/auth_handler.go
@@ -0,0 +1,225 @@
+/**
+ * package api
+ * 作用:认证相关API处理器
+ */
+package api
+
+import (
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/service"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * LoginHandler
+ * 功能:用户登录
+ * 路径:POST /api/login
+ */
+func LoginHandler(c *gin.Context) {
+ var req model.LoginReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ // 获取客户端IP
+ ip := getClientIP(c)
+
+ // 调用认证服务登录
+ user, err := service.AuthSvc.Login(req.Account, req.Password)
+ if err != nil {
+ // 记录登录失败日志
+ service.LoginLogSvc.LogLogin(req.Account, "0", ip, false)
+ utils.Unauthorized(c, err.Error())
+ return
+ }
+
+ // 生成Token
+ token, err := utils.GenerateToken(user.ID)
+ if err != nil {
+ // 记录登录失败日志
+ service.LoginLogSvc.LogLogin(req.Account, "0", ip, false)
+ utils.InternalError(c, "生成Token失败")
+ return
+ }
+
+ // 清除密码字段
+ user.Password = ""
+
+ // 记录登录成功日志
+ service.LoginLogSvc.LogLogin(req.Account, user.ID, ip, true)
+
+ utils.SuccessWithData(c, model.LoginResponse{
+ Token: token,
+ User: *user,
+ }, "登录成功")
+}
+
+// getClientIP 获取客户端IP
+func getClientIP(c *gin.Context) string {
+ // 优先从X-Forwarded-For获取
+ ip := c.GetHeader("X-Forwarded-For")
+ if ip != "" {
+ return ip
+ }
+
+ // 从X-Real-IP获取
+ ip = c.GetHeader("X-Real-IP")
+ if ip != "" {
+ return ip
+ }
+
+ // 从RemoteAddr获取
+ return c.ClientIP()
+}
+
+/**
+ * RegisterHandler
+ * 功能:用户注册
+ * 路径:POST /api/register
+ */
+func RegisterHandler(c *gin.Context) {
+ var req model.RegisterReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ // 如果提供了验证码,验证验证码
+ if req.Code != "" {
+ // 根据邮箱或手机号确定类型
+ codeType := "email"
+ if len(req.Phone) > 0 {
+ codeType = "sms"
+ }
+ target := req.Email
+ if codeType == "sms" {
+ target = req.Phone
+ }
+
+ valid, err := service.AuthSvc.VerifyCode(target, req.Code, codeType)
+ if err != nil || !valid {
+ utils.BadRequest(c, "验证码无效或已过期")
+ return
+ }
+ }
+
+ // 调用认证服务注册
+ user, err := service.AuthSvc.Register(&req)
+ if err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ // 生成Token
+ token, err := utils.GenerateToken(user.ID)
+ if err != nil {
+ utils.InternalError(c, "生成Token失败")
+ return
+ }
+
+ utils.SuccessWithData(c, model.RegisterResponse{
+ Token: token,
+ User: *user,
+ }, "注册成功")
+}
+
+/**
+ * LogoutHandler
+ * 功能:用户登出(可选,前端清除token)
+ * 路径:POST /api/logout
+ */
+func LogoutHandler(c *gin.Context) {
+ // 登出主要是前端清除token,后端可以记录日志或清除session
+ utils.Success(c, "登出成功")
+}
+
+/**
+ * CheckTokenHandler
+ * 功能:检查Token有效性
+ * 路径:GET /api/check-token
+ */
+func CheckTokenHandler(c *gin.Context) {
+ token := c.GetHeader("Authorization")
+ if token == "" {
+ utils.Unauthorized(c, "缺少Token")
+ return
+ }
+
+ // 移除 "Bearer " 前缀(如果存在)
+ if len(token) > 7 && token[:7] == "Bearer " {
+ token = token[7:]
+ }
+
+ userID, err := utils.ValidateToken(token)
+ if err != nil {
+ utils.Unauthorized(c, "Token无效或已过期")
+ return
+ }
+
+ utils.SuccessWithData(c, gin.H{
+ "status": "valid",
+ "user_id": userID,
+ }, "Token有效")
+}
+
+/**
+ * SendEmailCodeHandler
+ * 功能:发送邮箱验证码
+ * 路径:POST /api/send-email-code
+ */
+func SendEmailCodeHandler(c *gin.Context) {
+ var req model.SendCodeReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ if req.Type != "email" {
+ utils.BadRequest(c, "类型必须是email")
+ return
+ }
+
+ code, err := service.AuthSvc.SendEmailCode(req.Target)
+ if err != nil {
+ utils.InternalError(c, err.Error())
+ return
+ }
+
+ // 开发环境返回验证码,生产环境不应返回
+ utils.SuccessWithData(c, gin.H{
+ "code": code, // 仅开发环境,生产环境应移除
+ }, "验证码已发送")
+}
+
+/**
+ * SendSmsCodeHandler
+ * 功能:发送短信验证码
+ * 路径:POST /api/send-sms-code
+ */
+func SendSmsCodeHandler(c *gin.Context) {
+ var req model.SendCodeReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ if req.Type != "sms" {
+ utils.BadRequest(c, "类型必须是sms")
+ return
+ }
+
+ code, err := service.AuthSvc.SendSmsCode(req.Target)
+ if err != nil {
+ utils.InternalError(c, err.Error())
+ return
+ }
+
+ // 开发环境返回验证码,生产环境不应返回
+ utils.SuccessWithData(c, gin.H{
+ "code": code, // 仅开发环境,生产环境应移除
+ }, "验证码已发送")
+}
+
diff --git a/internal/api/contact_handler.go b/internal/api/contact_handler.go
new file mode 100644
index 0000000..8d3d4e8
--- /dev/null
+++ b/internal/api/contact_handler.go
@@ -0,0 +1,362 @@
+/**
+ * package api
+ * 作用:联系人管理相关API处理器
+ */
+package api
+
+import (
+ "strconv"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/service"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * ContactListHandler
+ * 功能:获取联系人列表(已存在,需改为从数据库查询)
+ * 路径:GET /api/contacts
+ */
+func ContactListHandler(c *gin.Context) {
+ // 从Context获取用户ID(由JWT中间件注入)
+ userID, exists := c.Get("user_id")
+ if !exists {
+ // 如果没有认证,返回空列表(兼容旧代码)
+ utils.SuccessWithData(c, []interface{}{}, "获取成功")
+ return
+ }
+
+ contacts, err := service.ContactSvc.GetContactsWithUserInfo(userID.(string))
+ if err != nil {
+ utils.InternalError(c, "查询失败")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if contacts == nil {
+ contacts = []map[string]interface{}{}
+ }
+
+ utils.SuccessWithData(c, contacts, "获取成功")
+}
+
+/**
+ * SearchUsersHandler
+ * 功能:搜索用户
+ * 路径:GET /api/contacts/search
+ */
+func SearchUsersHandler(c *gin.Context) {
+ keyword := c.Query("keyword")
+ if keyword == "" {
+ utils.BadRequest(c, "搜索关键词不能为空")
+ return
+ }
+
+ limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
+ if limit < 1 || limit > 100 {
+ limit = 20
+ }
+
+ users, err := service.ContactSvc.SearchUsers(keyword, limit)
+ if err != nil {
+ utils.InternalError(c, "搜索失败")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if users == nil {
+ users = []model.User{}
+ }
+
+ utils.SuccessWithData(c, users, "搜索成功")
+}
+
+/**
+ * AddFriendHandler
+ * 功能:添加好友(发送申请)
+ * 路径:POST /api/contacts/add-friend
+ */
+func AddFriendHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ var req struct {
+ ToUserID string `json:"to_user_id" binding:"required"`
+ Message string `json:"message"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ if err := service.ContactSvc.AddFriend(userID.(string), req.ToUserID, req.Message); err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ utils.Success(c, "好友申请已发送")
+}
+
+/**
+ * GetFriendRequestsHandler
+ * 功能:获取好友申请列表
+ * 路径:GET /api/contacts/friend-requests
+ */
+func GetFriendRequestsHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ requests, err := service.ContactSvc.GetFriendRequests(userID.(string))
+ if err != nil {
+ utils.InternalError(c, "查询失败")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if requests == nil {
+ requests = []model.FriendRequest{}
+ }
+
+ utils.SuccessWithData(c, requests, "获取成功")
+}
+
+/**
+ * AcceptFriendRequestHandler
+ * 功能:接受好友申请
+ * 路径:POST /api/contacts/accept-request
+ */
+func AcceptFriendRequestHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ var req struct {
+ RequestID uint `json:"request_id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误")
+ return
+ }
+
+ if err := service.ContactSvc.AcceptFriendRequest(req.RequestID, userID.(string)); err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ utils.Success(c, "已接受好友申请")
+}
+
+/**
+ * RejectFriendRequestHandler
+ * 功能:拒绝好友申请
+ * 路径:POST /api/contacts/reject-request
+ */
+func RejectFriendRequestHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ var req struct {
+ RequestID uint `json:"request_id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误")
+ return
+ }
+
+ if err := service.ContactSvc.RejectFriendRequest(req.RequestID, userID.(string)); err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ utils.Success(c, "已拒绝好友申请")
+}
+
+/**
+ * GetGroupsHandler
+ * 功能:获取分组列表
+ * 路径:GET /api/contacts/groups
+ */
+func GetGroupsHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ groups, err := service.ContactSvc.GetGroups(userID.(string))
+ if err != nil {
+ utils.InternalError(c, "查询失败")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if groups == nil {
+ groups = []model.ContactGroup{}
+ }
+
+ utils.SuccessWithData(c, groups, "获取成功")
+}
+
+/**
+ * CreateGroupHandler
+ * 功能:创建分组
+ * 路径:POST /api/contacts/groups
+ */
+func CreateGroupHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ var req struct {
+ GroupName string `json:"group_name" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ group, err := service.ContactSvc.CreateGroup(userID.(string), req.GroupName)
+ if err != nil {
+ utils.BadRequest(c, "创建失败: "+err.Error())
+ return
+ }
+
+ utils.SuccessWithData(c, group, "创建成功")
+}
+
+/**
+ * UpdateGroupHandler
+ * 功能:更新分组
+ * 路径:PUT /api/contacts/groups/:id
+ */
+func UpdateGroupHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ groupID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
+
+ var req struct {
+ GroupName string `json:"group_name"`
+ SortOrder int `json:"sort_order"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误")
+ return
+ }
+
+ updates := make(map[string]interface{})
+ if req.GroupName != "" {
+ updates["group_name"] = req.GroupName
+ }
+ if req.SortOrder > 0 {
+ updates["sort_order"] = req.SortOrder
+ }
+
+ if err := service.ContactSvc.UpdateGroup(uint(groupID), userID.(string), updates); err != nil {
+ utils.BadRequest(c, "更新失败: "+err.Error())
+ return
+ }
+
+ utils.Success(c, "更新成功")
+}
+
+/**
+ * DeleteGroupHandler
+ * 功能:删除分组
+ * 路径:DELETE /api/contacts/groups/:id
+ */
+func DeleteGroupHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ groupID, _ := strconv.ParseUint(c.Param("id"), 10, 32)
+
+ if err := service.ContactSvc.DeleteGroup(uint(groupID), userID.(string)); err != nil {
+ utils.BadRequest(c, "删除失败: "+err.Error())
+ return
+ }
+
+ utils.Success(c, "删除成功")
+}
+
+/**
+ * GetContactDetailHandler
+ * 功能:获取好友详情
+ * 路径:GET /api/contacts/:id
+ */
+func GetContactDetailHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ contactID := c.Param("id")
+
+ contact, err := service.ContactSvc.GetContactDetail(userID.(string), contactID)
+ if err != nil {
+ utils.NotFound(c, "好友不存在")
+ return
+ }
+
+ // 获取联系人用户信息
+ user, err := service.UserSvc.GetUserByID(contactID)
+ if err != nil {
+ utils.NotFound(c, "用户不存在")
+ return
+ }
+
+ result := map[string]interface{}{
+ "contact": contact,
+ "user": user,
+ }
+
+ utils.SuccessWithData(c, result, "获取成功")
+}
+
+/**
+ * UpdateContactHandler
+ * 功能:更新好友信息
+ * 路径:PUT /api/contacts/:id
+ */
+func UpdateContactHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ contactID := c.Param("id")
+
+ var req struct {
+ RemarkName string `json:"remark_name"`
+ GroupID uint `json:"group_id"`
+ IsTop *bool `json:"is_top"`
+ IsMuted *bool `json:"is_muted"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误")
+ return
+ }
+
+ updates := make(map[string]interface{})
+ if req.RemarkName != "" {
+ updates["remark_name"] = req.RemarkName
+ }
+ if req.GroupID > 0 {
+ updates["group_id"] = req.GroupID
+ }
+ if req.IsTop != nil {
+ updates["is_top"] = *req.IsTop
+ }
+ if req.IsMuted != nil {
+ updates["is_muted"] = *req.IsMuted
+ }
+
+ if err := service.ContactSvc.UpdateContact(userID.(string), contactID, updates); err != nil {
+ utils.BadRequest(c, "更新失败: "+err.Error())
+ return
+ }
+
+ utils.Success(c, "更新成功")
+}
+
+/**
+ * DeleteContactHandler
+ * 功能:删除好友
+ * 路径:DELETE /api/contacts/:id
+ */
+func DeleteContactHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+ contactID := c.Param("id")
+
+ if err := service.ContactSvc.DeleteContact(userID.(string), contactID); err != nil {
+ utils.BadRequest(c, "删除失败: "+err.Error())
+ return
+ }
+
+ utils.Success(c, "已删除好友")
+}
+
diff --git a/internal/api/handler.go b/internal/api/handler.go
index ed8a038..2c7398e 100644
--- a/internal/api/handler.go
+++ b/internal/api/handler.go
@@ -7,12 +7,13 @@ package api
import (
"fmt"
- "net/http"
+ "strconv"
"time"
"xk-websocket-v2/internal/manager"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/turnserver"
+ "xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
@@ -33,7 +34,7 @@ func SendHandler(c *gin.Context) {
var req model.SendMessageReq
// 1. 绑定并校验 JSON 参数
if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "无效的JSON参数"})
+ utils.BadRequest(c, "无效的JSON参数")
return
}
@@ -63,7 +64,7 @@ func SendHandler(c *gin.Context) {
service.ChatSvc.HandleUserMessage(mockClient, &req)
// 6. 返回成功响应
- c.JSON(http.StatusOK, gin.H{"status": "ok"})
+ utils.Success(c, "消息已发送")
}
/**
@@ -89,12 +90,12 @@ func SendToUserHandler(c *gin.Context) {
func BindHandler(c *gin.Context) {
var req model.BindReq
if err := c.ShouldBindJSON(&req); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
+ utils.BadRequest(c, "参数错误")
return
}
// 调用服务层进行绑定
service.ChatSvc.BindUserByClientID(req.ClientID, req.UserID)
- c.JSON(http.StatusOK, gin.H{"status": "success"})
+ utils.Success(c, "绑定成功")
}
/**
@@ -106,7 +107,7 @@ func CheckUserOnlineHandler(c *gin.Context) {
userID := c.Query("user_id")
// 调用服务层查询 Redis
isOnline := service.ChatSvc.IsUserOnline(userID)
- c.JSON(http.StatusOK, gin.H{"status": "success", "result": isOnline})
+ utils.SuccessWithData(c, gin.H{"is_online": isOnline}, "查询成功")
}
// ==========================================
@@ -120,18 +121,52 @@ func CheckUserOnlineHandler(c *gin.Context) {
*/
func HistoryHandler(c *gin.Context) {
roomID := c.Query("room_id")
- var msgs []model.ChatMessage
-
- // 简单查询最近 50 条
- // 生产环境应添加 page, page_size 参数
- result := service.ChatSvc.DB.Where("room_id = ?", roomID).
- Order("created_at desc").Limit(50).Find(&msgs)
-
- if result.Error != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"})
+ if roomID == "" {
+ utils.BadRequest(c, "room_id参数必填")
return
}
- c.JSON(http.StatusOK, gin.H{"data": msgs})
+
+ // 分页参数
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
+
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 100 {
+ pageSize = 50
+ }
+
+ var msgs []model.ChatMessage
+ var total int64
+
+ // 获取总数
+ service.ChatSvc.DB.Model(&model.ChatMessage{}).Where("room_id = ?", roomID).Count(&total)
+
+ // 分页查询
+ offset := (page - 1) * pageSize
+ result := service.ChatSvc.DB.Where("room_id = ?", roomID).
+ Order("created_at desc").
+ Offset(offset).
+ Limit(pageSize).
+ Find(&msgs)
+
+ if result.Error != nil {
+ utils.InternalError(c, "db error")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if msgs == nil {
+ msgs = []model.ChatMessage{}
+ }
+
+ utils.SuccessWithData(c, gin.H{
+ "data": msgs,
+ "total": total,
+ "page": page,
+ "size": pageSize,
+ }, "获取成功")
}
/**
@@ -143,28 +178,6 @@ func SyncMessagesHandler(c *gin.Context) {
HistoryHandler(c)
}
-/**
- * ContactListHandler
- * 功能:获取联系人列表 (模拟数据)。
- * 路径:GET /api/contacts
- * 说明:用于前端展示登录后的好友列表。
- */
-func ContactListHandler(c *gin.Context) {
- // 模拟 10 个用户数据
- users := []model.UserContact{
- {ID: "1001", Name: "张三 (我)", Avatar: "张", Desc: "Golang 专家"},
- {ID: "1002", Name: "李琦 (妻)", Avatar: "李", Desc: "在线"},
- {ID: "1003", Name: "王医生", Avatar: "医", Desc: "主任医师"},
- {ID: "1004", Name: "客服小蜜", Avatar: "客", Desc: "全天在线"},
- {ID: "1005", Name: "技术支持", Avatar: "技", Desc: "请重启试试"},
- {ID: "1006", Name: "财务小赵", Avatar: "财", Desc: "报销单请提交"},
- {ID: "1007", Name: "运维阿强", Avatar: "运", Desc: "服务器维护中"},
- {ID: "1008", Name: "测试小丽", Avatar: "测", Desc: "Bug 太多了"},
- {ID: "1009", Name: "HR", Avatar: "人", Desc: "本月考勤异常"},
- {ID: "1010", Name: "老板", Avatar: "老", Desc: "今晚开会"},
- }
- c.JSON(http.StatusOK, gin.H{"data": users})
-}
// ==========================================
// 系统与 WebRTC 接口
@@ -176,11 +189,11 @@ func ContactListHandler(c *gin.Context) {
* 路径:GET /api/health
*/
func HealthHandler(c *gin.Context) {
- c.JSON(http.StatusOK, gin.H{
+ utils.SuccessWithData(c, gin.H{
"status": "ok",
"node": viper.GetString("app.node_id"),
"time": time.Now().Format(time.RFC3339),
- })
+ }, "服务正常")
}
/**
@@ -205,5 +218,5 @@ func ICEHandler(c *gin.Context) {
Username: username,
Credential: credential,
}
- c.JSON(http.StatusOK, gin.H{"data": []model.ICEServerConfig{cfg}})
+ utils.SuccessWithData(c, []model.ICEServerConfig{cfg}, "获取成功")
}
diff --git a/internal/api/room_handler.go b/internal/api/room_handler.go
new file mode 100644
index 0000000..9840e68
--- /dev/null
+++ b/internal/api/room_handler.go
@@ -0,0 +1,71 @@
+/**
+ * package api
+ * 作用:房间管理相关API处理器
+ */
+package api
+
+import (
+ "xk-websocket-v2/internal/service"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * CreateRoomHandler
+ * 功能:创建房间
+ * 路径:POST /api/rooms
+ */
+func CreateRoomHandler(c *gin.Context) {
+ userID, _ := c.Get("user_id")
+
+ var req struct {
+ RoomType string `json:"room_type" binding:"required,oneof=p2p group"`
+ Members []string `json:"members" binding:"required"`
+ RoomName string `json:"room_name"`
+ RoomAvatar string `json:"room_avatar"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ room, err := service.RoomSvc.CreateRoom(req.RoomType, req.Members, userID.(string))
+ if err != nil {
+ utils.BadRequest(c, err.Error())
+ return
+ }
+
+ // 如果提供了房间名称和头像,更新
+ if req.RoomName != "" || req.RoomAvatar != "" {
+ updates := make(map[string]interface{})
+ if req.RoomName != "" {
+ updates["room_name"] = req.RoomName
+ }
+ if req.RoomAvatar != "" {
+ updates["room_avatar"] = req.RoomAvatar
+ }
+ service.RoomSvc.DB.Model(room).Updates(updates)
+ }
+
+ utils.SuccessWithData(c, room, "创建成功")
+}
+
+/**
+ * GetRoomHandler
+ * 功能:获取房间信息
+ * 路径:GET /api/rooms/:id
+ */
+func GetRoomHandler(c *gin.Context) {
+ roomID := c.Param("id")
+
+ room, err := service.RoomSvc.GetRoom(roomID)
+ if err != nil {
+ utils.NotFound(c, "房间不存在")
+ return
+ }
+
+ utils.SuccessWithData(c, room, "获取成功")
+}
+
diff --git a/internal/api/user_handler.go b/internal/api/user_handler.go
new file mode 100644
index 0000000..df91dba
--- /dev/null
+++ b/internal/api/user_handler.go
@@ -0,0 +1,155 @@
+/**
+ * package api
+ * 作用:用户管理相关API处理器
+ */
+package api
+
+import (
+ "fmt"
+ "math/rand"
+ "strconv"
+ "time"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/service"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * GetMyInfoHandler
+ * 功能:获取当前用户信息
+ * 路径:GET /api/user/my-info
+ * 需要:JWT认证
+ */
+func GetMyInfoHandler(c *gin.Context) {
+ // 从Context获取用户ID(由JWT中间件注入)
+ userID, exists := c.Get("user_id")
+ if !exists {
+ utils.Unauthorized(c, "未认证")
+ return
+ }
+
+ user, err := service.UserSvc.GetUserByID(userID.(string))
+ if err != nil {
+ utils.NotFound(c, "用户不存在")
+ return
+ }
+
+ utils.SuccessWithData(c, user, "获取成功")
+}
+
+/**
+ * GetUserListHandler
+ * 功能:获取用户列表
+ * 路径:GET /api/user/list
+ */
+func GetUserListHandler(c *gin.Context) {
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
+
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 100 {
+ pageSize = 20
+ }
+
+ users, total, err := service.UserSvc.GetUserList(page, pageSize)
+ if err != nil {
+ utils.InternalError(c, "查询失败")
+ return
+ }
+
+ // 确保返回空数组而不是null
+ if users == nil {
+ users = []model.User{}
+ }
+
+ utils.SuccessWithData(c, gin.H{
+ "data": users,
+ "total": total,
+ "page": page,
+ "size": pageSize,
+ }, "获取成功")
+}
+
+/**
+ * CreateUserHandler
+ * 功能:创建用户(管理员)
+ * 路径:POST /api/user/create
+ */
+func CreateUserHandler(c *gin.Context) {
+ var user model.User
+ if err := c.ShouldBindJSON(&user); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ // 生成用户ID
+ user.ID = generateUserID()
+
+ if err := service.UserSvc.CreateUser(&user); err != nil {
+ utils.BadRequest(c, "创建用户失败: "+err.Error())
+ return
+ }
+
+ user.Password = "" // 清除密码
+ utils.SuccessWithData(c, user, "创建成功")
+}
+
+/**
+ * UpdateUserHandler
+ * 功能:更新用户信息
+ * 路径:POST /api/user/update
+ */
+func UpdateUserHandler(c *gin.Context) {
+ var req struct {
+ ID string `json:"id" binding:"required"`
+ Updates map[string]interface{} `json:"updates" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误: "+err.Error())
+ return
+ }
+
+ if err := service.UserSvc.UpdateUser(req.ID, req.Updates); err != nil {
+ utils.BadRequest(c, "更新失败: "+err.Error())
+ return
+ }
+
+ utils.Success(c, "更新成功")
+}
+
+/**
+ * DeleteUserHandler
+ * 功能:删除用户
+ * 路径:POST /api/user/delete
+ */
+func DeleteUserHandler(c *gin.Context) {
+ var req struct {
+ ID string `json:"id" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ utils.BadRequest(c, "参数错误")
+ return
+ }
+
+ if err := service.UserSvc.DeleteUser(req.ID); err != nil {
+ utils.BadRequest(c, "删除失败: "+err.Error())
+ return
+ }
+
+ utils.Success(c, "删除成功")
+}
+
+// 辅助函数:生成用户ID
+func generateUserID() string {
+ // 使用时间戳+随机数生成用户ID
+ timestamp := time.Now().UnixNano()
+ random := rand.Intn(1000000)
+ return fmt.Sprintf("user_%d_%d", timestamp, random)
+}
+
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
new file mode 100644
index 0000000..67e3720
--- /dev/null
+++ b/internal/middleware/auth.go
@@ -0,0 +1,112 @@
+/**
+ * package middleware
+ * 作用:JWT认证中间件
+ */
+package middleware
+
+import (
+ "net/http"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * JWTAuthMiddleware
+ *
+ * 功能:JWT认证中间件
+ *
+ * 作用:
+ * 1. 从HTTP请求头或查询参数中提取JWT Token
+ * 2. 验证Token的有效性和过期时间
+ * 3. 从Token中解析出用户ID
+ * 4. 将用户ID注入到Gin Context中,供后续处理器使用
+ * 5. 如果Token无效或缺失,返回401未授权错误
+ *
+ * 使用场景:
+ * - 需要用户登录才能访问的API接口
+ * - 需要在处理器中获取当前用户信息的接口
+ *
+ * @returns gin.HandlerFunc 中间件处理函数
+ */
+func JWTAuthMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // 步骤1: 从HTTP请求头中获取Token(标准方式)
+ token := c.GetHeader("Authorization")
+ if token == "" {
+ // 步骤2: 如果请求头中没有,尝试从查询参数获取(兼容旧代码)
+ token = c.Query("token")
+ }
+
+ // 步骤3: 如果仍然没有Token,返回401未授权错误
+ if token == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证Token"})
+ c.Abort() // 终止请求处理
+ return
+ }
+
+ // 步骤4: 移除 "Bearer " 前缀(如果存在)
+ // 标准JWT Token格式:Bearer
+ if len(token) > 7 && token[:7] == "Bearer " {
+ token = token[7:]
+ }
+
+ // 步骤5: 验证Token的有效性和过期时间
+ userID, err := utils.ValidateToken(token)
+ if err != nil {
+ // Token无效或已过期,返回401错误
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Token无效或已过期: " + err.Error()})
+ c.Abort()
+ return
+ }
+
+ // 步骤6: 将解析出的用户ID注入到Context中
+ // 后续处理器可以通过 c.Get("user_id") 获取当前用户ID
+ c.Set("user_id", userID)
+ c.Next() // 继续执行下一个中间件或处理器
+ }
+}
+
+/**
+ * OptionalJWTAuthMiddleware
+ *
+ * 功能:可选的JWT认证中间件(不强制要求认证)
+ *
+ * 作用:
+ * 1. 如果请求中提供了Token,则验证Token并注入用户ID
+ * 2. 如果没有提供Token,则继续执行,不返回错误
+ * 3. 适用于既支持登录用户访问,也支持匿名用户访问的接口
+ *
+ * 使用场景:
+ * - 公开接口,但登录用户可以获取更多信息
+ * - 兼容旧代码,不强制要求认证
+ *
+ * @returns gin.HandlerFunc 中间件处理函数
+ */
+func OptionalJWTAuthMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // 步骤1: 尝试从请求头或查询参数获取Token
+ token := c.GetHeader("Authorization")
+ if token == "" {
+ token = c.Query("token")
+ }
+
+ // 步骤2: 如果提供了Token,则验证并注入用户ID
+ if token != "" {
+ // 移除 "Bearer " 前缀
+ if len(token) > 7 && token[:7] == "Bearer " {
+ token = token[7:]
+ }
+
+ // 验证Token,如果有效则注入用户ID
+ // 如果Token无效,不返回错误,继续执行(允许匿名访问)
+ if userID, err := utils.ValidateToken(token); err == nil {
+ c.Set("user_id", userID)
+ }
+ }
+
+ // 步骤3: 继续执行,无论是否有Token
+ c.Next()
+ }
+}
+
diff --git a/internal/middleware/request_log.go b/internal/middleware/request_log.go
new file mode 100644
index 0000000..fb49f03
--- /dev/null
+++ b/internal/middleware/request_log.go
@@ -0,0 +1,173 @@
+/**
+ * package middleware
+ * 作用:接口请求日志中间件
+ * 说明:记录所有API请求信息,异步写入数据库
+ */
+package middleware
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "strings"
+ "time"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// RequestLogMiddleware 请求日志中间件
+type RequestLogMiddleware struct {
+ DB *gorm.DB
+}
+
+// NewRequestLogMiddleware 创建请求日志中间件
+func NewRequestLogMiddleware(db *gorm.DB) *RequestLogMiddleware {
+ return &RequestLogMiddleware{DB: db}
+}
+
+// Handler 中间件处理函数
+func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // 跳过OPTIONS请求
+ if c.Request.Method == "OPTIONS" {
+ c.Next()
+ return
+ }
+
+ // 获取请求IP
+ ip := getClientIP(c)
+
+ // 本地IP不记录
+ if utils.GetIPLocation(ip) == "本地" {
+ c.Next()
+ return
+ }
+
+ // 获取请求参数
+ var requestBody []byte
+ if c.Request.Body != nil {
+ requestBody, _ = io.ReadAll(c.Request.Body)
+ c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
+ }
+
+ // 获取用户ID(从Context中获取,未登录为"0")
+ userID := "0"
+ if uid, exists := c.Get("user_id"); exists {
+ if uidStr, ok := uid.(string); ok {
+ userID = uidStr
+ }
+ }
+
+ // 记录开始时间
+ startTime := time.Now()
+
+ // 创建响应写入器
+ writer := &responseWriter{
+ ResponseWriter: c.Writer,
+ body: &bytes.Buffer{},
+ }
+ c.Writer = writer
+
+ // 处理请求
+ c.Next()
+
+ // 计算请求时间
+ duration := time.Since(startTime)
+
+ // 异步记录日志(避免影响性能)
+ go m.logRequest(c, ip, userID, requestBody, writer.body.Bytes(), writer.status, duration)
+ }
+}
+
+// logRequest 记录请求日志
+func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, requestBody, responseBody []byte, httpStatus int, duration time.Duration) {
+ // 获取IP归属地
+ location := utils.GetIPLocation(ip)
+
+ // 获取响应code(从响应体中解析)
+ responseCode := 0
+ if len(responseBody) > 0 {
+ // 尝试解析响应体获取code
+ var resp model.ApiResponse
+ if err := json.Unmarshal(responseBody, &resp); err == nil {
+ responseCode = resp.Code
+ }
+ }
+
+ // 限制请求参数长度(避免存储过大)
+ requestParams := string(requestBody)
+ if len(requestParams) > 5000 {
+ requestParams = requestParams[:5000] + "...(truncated)"
+ }
+
+ // 限制返回参数长度
+ responseParams := string(responseBody)
+ if len(responseParams) > 5000 {
+ responseParams = responseParams[:5000] + "...(truncated)"
+ }
+
+ // 创建日志记录
+ log := model.ApiRequestLog{
+ Route: c.FullPath(),
+ IP: ip,
+ IPLocation: location,
+ UserID: userID,
+ Method: c.Request.Method,
+ RequestParams: requestParams,
+ ResponseParams: responseParams,
+ ResponseCode: responseCode,
+ HTTPStatus: httpStatus,
+ RequestTime: time.Now(),
+ }
+
+ // 异步写入数据库
+ m.DB.Create(&log)
+}
+
+// getClientIP 获取客户端IP
+func getClientIP(c *gin.Context) string {
+ // 优先从X-Forwarded-For获取
+ ip := c.GetHeader("X-Forwarded-For")
+ if ip != "" {
+ // X-Forwarded-For可能包含多个IP,取第一个
+ ips := strings.Split(ip, ",")
+ if len(ips) > 0 {
+ return strings.TrimSpace(ips[0])
+ }
+ }
+
+ // 从X-Real-IP获取
+ ip = c.GetHeader("X-Real-IP")
+ if ip != "" {
+ return ip
+ }
+
+ // 从RemoteAddr获取
+ return c.ClientIP()
+}
+
+// responseWriter 响应写入器(用于捕获响应内容)
+type responseWriter struct {
+ gin.ResponseWriter
+ body *bytes.Buffer
+ status int
+}
+
+func (w *responseWriter) Write(b []byte) (int, error) {
+ w.body.Write(b)
+ return w.ResponseWriter.Write(b)
+}
+
+func (w *responseWriter) WriteString(s string) (int, error) {
+ w.body.WriteString(s)
+ return w.ResponseWriter.WriteString(s)
+}
+
+func (w *responseWriter) WriteHeader(statusCode int) {
+ w.status = statusCode
+ w.ResponseWriter.WriteHeader(statusCode)
+}
+
diff --git a/internal/middleware/response.go b/internal/middleware/response.go
new file mode 100644
index 0000000..a1ed984
--- /dev/null
+++ b/internal/middleware/response.go
@@ -0,0 +1,26 @@
+/**
+ * package middleware
+ * 作用:响应时间统计中间件
+ * 说明:记录请求开始时间,用于计算响应时间
+ */
+package middleware
+
+import (
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+/**
+ * ResponseTimeMiddleware
+ * 作用:记录请求开始时间,用于后续计算响应时间
+ */
+func ResponseTimeMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // 记录请求开始时间
+ c.Set("request_start_time", time.Now())
+ // 继续处理请求
+ c.Next()
+ }
+}
+
diff --git a/internal/model/response.go b/internal/model/response.go
new file mode 100644
index 0000000..3a96a51
--- /dev/null
+++ b/internal/model/response.go
@@ -0,0 +1,28 @@
+/**
+ * package model
+ * 作用:定义统一的API响应格式
+ */
+package model
+
+/**
+ * InterfaceInfo
+ * 作用:接口信息,包含响应时间和服务器标识
+ */
+type InterfaceInfo struct {
+ ResultTime string `json:"result_time"` // 响应时间,格式:XX ms
+ Ecs string `json:"ecs"` // 服务器标识
+}
+
+/**
+ * ApiResponse
+ * 作用:统一的API响应结构体
+ * 说明:所有API响应都使用此格式,HTTP状态码统一返回200,错误通过code字段标识
+ */
+type ApiResponse struct {
+ Code int `json:"code"` // 业务状态码:0=成功,非0=失败
+ Message string `json:"message"` // 响应消息
+ Result interface{} `json:"result"` // 响应数据
+ Type string `json:"type"` // 响应类型:"success" 或 "error"
+ InterfaceInfo InterfaceInfo `json:"interface_info"` // 接口信息
+}
+
diff --git a/internal/model/types.go b/internal/model/types.go
index 8d249fb..9bbe722 100644
--- a/internal/model/types.go
+++ b/internal/model/types.go
@@ -41,6 +41,11 @@ type ChatMessage struct {
CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
}
+// TableName 指定表名和注释
+func (ChatMessage) TableName() string {
+ return "chat_messages"
+}
+
// ==========================================
// 交互数据传输对象 (DTO - Data Transfer Object)
// ==========================================
@@ -125,19 +130,281 @@ type ICEServerConfig struct {
Credential string `json:"credential,omitempty"`
}
+/**
+ * User
+ * 对应数据库表:users
+ * 作用:用户基本信息表
+ */
+type User struct {
+ // 用户唯一ID
+ ID string `gorm:"primaryKey;type:varchar(100);comment:用户唯一ID" json:"id"`
+ // 邮箱
+ Email string `gorm:"type:varchar(255);uniqueIndex;comment:邮箱" json:"email"`
+ // 手机号
+ Phone string `gorm:"type:varchar(20);uniqueIndex;comment:手机号" json:"phone"`
+ // 密码(加密后)
+ Password string `gorm:"type:varchar(255);comment:密码(加密后)" json:"-"`
+ // 用户名称
+ Name string `gorm:"type:varchar(100);comment:用户名称" json:"name"`
+ // 用户头像URL或字符
+ Avatar string `gorm:"type:varchar(500);comment:用户头像" json:"avatar"`
+ // 用户描述或签名
+ Desc string `gorm:"type:varchar(500);comment:用户描述或签名" json:"desc"`
+ // 地区
+ Region string `gorm:"type:varchar(100);comment:地区" json:"region"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+ // 更新时间
+ UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
+}
+
+// TableName 指定表名和注释
+func (User) TableName() string {
+ return "users"
+}
+
/**
* UserContact
- * 作用:模拟联系人结构
+ * 对应数据库表:user_contacts
+ * 作用:用户联系人表,存储好友关系、分组、备注等信息
*/
type UserContact struct {
- // 用户唯一ID
- ID string `json:"id"`
- // 用户名称
- Name string `json:"name"`
- // 用户头像URL或字符
- Avatar string `json:"avatar"`
- // 用户描述或签名
- Desc string `json:"desc"`
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 用户ID - 当前用户
+ UserID string `gorm:"type:varchar(100);index;comment:用户ID" json:"user_id"`
+ // 联系人ID - 好友的用户ID
+ ContactID string `gorm:"type:varchar(100);index;comment:联系人ID" json:"contact_id"`
+ // 备注名称 - 用户自定义的好友备注
+ RemarkName string `gorm:"type:varchar(100);comment:备注名称" json:"remark_name"`
+ // 分组ID - 好友所属分组
+ GroupID uint `gorm:"type:int;index;comment:分组ID" json:"group_id"`
+ // 是否置顶
+ IsTop bool `gorm:"type:tinyint(1);default:0;comment:是否置顶" json:"is_top"`
+ // 是否免打扰
+ IsMuted bool `gorm:"type:tinyint(1);default:0;comment:是否免打扰" json:"is_muted"`
+ // 最后聊天时间
+ LastChatTime *time.Time `gorm:"type:datetime;comment:最后聊天时间" json:"last_chat_time"`
+ // 最后一条消息
+ LastMessage string `gorm:"type:text;comment:最后一条消息" json:"last_message"`
+ // 未读消息数
+ UnreadCount int `gorm:"type:int;default:0;comment:未读消息数" json:"unread_count"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+ // 更新时间
+ UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
+}
+
+// TableName 指定表名和注释
+func (UserContact) TableName() string {
+ return "user_contacts"
+}
+
+/**
+ * ContactGroup
+ * 对应数据库表:contact_groups
+ * 作用:联系人分组表
+ */
+type ContactGroup struct {
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 用户ID - 分组所属用户
+ UserID string `gorm:"type:varchar(100);index;comment:用户ID" json:"user_id"`
+ // 分组名称
+ GroupName string `gorm:"type:varchar(100);comment:分组名称" json:"group_name"`
+ // 排序顺序
+ SortOrder int `gorm:"type:int;default:0;comment:排序顺序" json:"sort_order"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+}
+
+// TableName 指定表名和注释
+func (ContactGroup) TableName() string {
+ return "contact_groups"
+}
+
+/**
+ * ChatRoom
+ * 对应数据库表:chat_rooms
+ * 作用:聊天房间表,支持点对点和群聊
+ */
+type ChatRoom struct {
+ // 房间ID - 主键
+ RoomID string `gorm:"primaryKey;type:varchar(100);comment:房间ID" json:"room_id"`
+ // 房间类型 - "p2p" 点对点 / "group" 群聊
+ RoomType string `gorm:"type:varchar(20);index;comment:房间类型" json:"room_type"`
+ // 房间名称 - 群聊时显示
+ RoomName string `gorm:"type:varchar(200);comment:房间名称" json:"room_name"`
+ // 房间头像 - 群聊时显示
+ RoomAvatar string `gorm:"type:varchar(500);comment:房间头像" json:"room_avatar"`
+ // 成员列表 - JSON数组
+ Members string `gorm:"type:text;comment:成员列表JSON" json:"members"`
+ // 创建者ID
+ CreatorID string `gorm:"type:varchar(100);index;comment:创建者ID" json:"creator_id"`
+ // 最后消息时间
+ LastMessageTime *time.Time `gorm:"type:datetime;index;comment:最后消息时间" json:"last_message_time"`
+ // 最后消息内容
+ LastMessage string `gorm:"type:text;comment:最后消息内容" json:"last_message"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+ // 更新时间
+ UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
+}
+
+// TableName 指定表名和注释
+func (ChatRoom) TableName() string {
+ return "chat_rooms"
+}
+
+/**
+ * FriendRequest
+ * 对应数据库表:friend_requests
+ * 作用:好友申请表
+ */
+type FriendRequest struct {
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 发送者用户ID
+ FromUserID string `gorm:"type:varchar(100);index;comment:发送者用户ID" json:"from_user_id"`
+ // 接收者用户ID
+ ToUserID string `gorm:"type:varchar(100);index;comment:接收者用户ID" json:"to_user_id"`
+ // 申请消息
+ Message string `gorm:"type:text;comment:申请消息" json:"message"`
+ // 状态 - "pending", "accepted", "rejected"
+ Status string `gorm:"type:varchar(20);default:'pending';index;comment:状态" json:"status"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+ // 更新时间
+ UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"`
+}
+
+// TableName 指定表名和注释
+func (FriendRequest) TableName() string {
+ return "friend_requests"
+}
+
+/**
+ * VerificationCode
+ * 对应数据库表:verification_codes
+ * 作用:验证码表
+ */
+type VerificationCode struct {
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 邮箱或手机号
+ Target string `gorm:"type:varchar(255);index;comment:邮箱或手机号" json:"target"`
+ // 验证码
+ Code string `gorm:"type:varchar(10);comment:验证码" json:"-"`
+ // 类型 - "email" 或 "sms"
+ Type string `gorm:"type:varchar(20);comment:类型" json:"type"`
+ // 过期时间
+ ExpiresAt time.Time `gorm:"type:datetime;index;comment:过期时间" json:"expires_at"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+}
+
+// TableName 指定表名和注释
+func (VerificationCode) TableName() string {
+ return "verification_codes"
+}
+
+/**
+ * Attachment
+ * 对应数据库表:attachments
+ * 作用:附件表,记录上传的文件信息
+ */
+type Attachment struct {
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 上传者ID
+ UploaderID string `gorm:"type:varchar(100);index;comment:上传者ID" json:"uploader_id"`
+ // 文件名
+ FileName string `gorm:"type:varchar(255);comment:文件名" json:"file_name"`
+ // 文件类型 - "image" 或 "video"
+ FileType string `gorm:"type:varchar(20);index;comment:文件类型" json:"file_type"`
+ // 文件大小(字节)
+ FileSize int64 `gorm:"type:bigint;comment:文件大小(字节)" json:"file_size"`
+ // 文件路径
+ FilePath string `gorm:"type:varchar(500);comment:文件路径" json:"file_path"`
+ // 文件URL
+ FileURL string `gorm:"type:varchar(500);comment:文件URL" json:"file_url"`
+ // MIME类型
+ MimeType string `gorm:"type:varchar(100);comment:MIME类型" json:"mime_type"`
+ // 宽度(图片/视频)
+ Width int `gorm:"type:int;comment:宽度" json:"width"`
+ // 高度(图片/视频)
+ Height int `gorm:"type:int;comment:高度" json:"height"`
+ // 时长(视频,秒)
+ Duration int `gorm:"type:int;comment:时长(秒)" json:"duration"`
+ // 创建时间
+ CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"`
+}
+
+// TableName 指定表名和注释
+func (Attachment) TableName() string {
+ return "attachments"
+}
+
+/**
+ * ApiRequestLog
+ * 对应数据库表:api_request_logs
+ * 作用:接口请求日志表,记录所有API请求信息
+ */
+type ApiRequestLog struct {
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 请求路由
+ Route string `gorm:"type:varchar(255);index;comment:请求路由" json:"route"`
+ // 请求IP
+ IP string `gorm:"type:varchar(50);index;comment:请求IP" json:"ip"`
+ // IP归属地
+ IPLocation string `gorm:"type:varchar(255);comment:IP归属地" json:"ip_location"`
+ // 请求用户ID(未登录为0)
+ UserID string `gorm:"type:varchar(100);index;comment:请求用户ID" json:"user_id"`
+ // 请求方式
+ Method string `gorm:"type:varchar(10);comment:请求方式" json:"method"`
+ // 请求参数(JSON)
+ RequestParams string `gorm:"type:text;comment:请求参数" json:"request_params"`
+ // 返回参数(JSON)
+ ResponseParams string `gorm:"type:text;comment:返回参数" json:"response_params"`
+ // 返回code
+ ResponseCode int `gorm:"type:int;index;comment:返回code" json:"response_code"`
+ // 返回http状态
+ HTTPStatus int `gorm:"type:int;comment:返回http状态" json:"http_status"`
+ // 请求时间
+ RequestTime time.Time `gorm:"type:datetime;index;comment:请求时间" json:"request_time"`
+}
+
+// TableName 指定表名和注释
+func (ApiRequestLog) TableName() string {
+ return "api_request_logs"
+}
+
+/**
+ * LoginLog
+ * 对应数据库表:login_logs
+ * 作用:登录日志表,记录所有登录尝试
+ */
+type LoginLog struct {
+ // 主键ID
+ ID uint `gorm:"primaryKey;comment:主键ID" json:"id"`
+ // 登录账号
+ Account string `gorm:"type:varchar(255);index;comment:登录账号" json:"account"`
+ // 登录ID(失败记录0)
+ UserID string `gorm:"type:varchar(100);index;comment:登录ID" json:"user_id"`
+ // 归属地(IP归属地)
+ Location string `gorm:"type:varchar(255);comment:归属地" json:"location"`
+ // 地址(IP地址)
+ IP string `gorm:"type:varchar(50);index;comment:IP地址" json:"ip"`
+ // 成功/失败状态(true=成功,false=失败)
+ Success bool `gorm:"type:tinyint(1);index;comment:成功状态" json:"success"`
+ // 登录时间
+ LoginTime time.Time `gorm:"type:datetime;index;comment:登录时间" json:"login_time"`
+}
+
+// TableName 指定表名和注释
+func (LoginLog) TableName() string {
+ return "login_logs"
}
/**
@@ -153,6 +420,75 @@ type ClusterMessage struct {
Payload interface{} `json:"payload"`
}
+// ==========================================
+// 认证相关 DTO
+// ==========================================
+
+/**
+ * LoginReq
+ * 作用:登录请求参数
+ */
+type LoginReq struct {
+ // 账号(邮箱或手机号)
+ Account string `json:"account" binding:"required"`
+ // 密码
+ Password string `json:"password" binding:"required"`
+ // 记住我
+ Remember bool `json:"remember,omitempty"`
+}
+
+/**
+ * RegisterReq
+ * 作用:注册请求参数
+ */
+type RegisterReq struct {
+ // 邮箱
+ Email string `json:"email" binding:"required,email"`
+ // 手机号
+ Phone string `json:"phone" binding:"required"`
+ // 密码
+ Password string `json:"password" binding:"required,min=8"`
+ // 确认密码
+ ConfirmPassword string `json:"confirm_password" binding:"required"`
+ // 验证码(可选)
+ Code string `json:"code,omitempty"`
+ // 同意用户协议
+ AgreeTerms bool `json:"agree_terms" binding:"required"`
+}
+
+/**
+ * LoginResponse
+ * 作用:登录响应
+ */
+type LoginResponse struct {
+ // Token
+ Token string `json:"token"`
+ // 用户信息
+ User User `json:"user"`
+}
+
+/**
+ * RegisterResponse
+ * 作用:注册响应
+ */
+type RegisterResponse struct {
+ // Token
+ Token string `json:"token"`
+ // 用户信息
+ User User `json:"user"`
+}
+
+/**
+ * SendCodeReq
+ * 作用:发送验证码请求
+ */
+type SendCodeReq struct {
+ // 邮箱或手机号
+ Target string `json:"target" binding:"required"`
+ // 类型 - "email" 或 "sms"
+ Type string `json:"type" binding:"required,oneof=email sms"`
+}
+
// ==========================================
// 常量定义
// ==========================================
@@ -162,4 +498,8 @@ const (
KeyUserNodeMap = "ws:user:node:"
// Redis Channel: 集群广播频道
ChanClusterBroadcast = "ws:cluster:broadcast"
+
+ // 文件大小限制(字节)
+ MaxImageSize = 10 * 1024 * 1024 // 10MB
+ MaxVideoSize = 500 * 1024 * 1024 // 500MB
)
diff --git a/internal/service/attachment_service.go b/internal/service/attachment_service.go
new file mode 100644
index 0000000..a209e24
--- /dev/null
+++ b/internal/service/attachment_service.go
@@ -0,0 +1,207 @@
+/**
+ * package service
+ * 作用:附件管理服务
+ */
+package service
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "mime"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+ "xk-websocket-v2/internal/model"
+
+ "gorm.io/gorm"
+)
+
+// AttachmentService 附件服务结构体
+type AttachmentService struct {
+ DB *gorm.DB
+}
+
+// AttachmentSvc 全局单例
+var AttachmentSvc *AttachmentService
+
+/**
+ * InitAttachmentService
+ * 功能:初始化附件服务
+ */
+func InitAttachmentService(db *gorm.DB) {
+ AttachmentSvc = &AttachmentService{DB: db}
+ // 创建上传目录
+ os.MkdirAll("./uploads/images", os.ModePerm)
+ os.MkdirAll("./uploads/videos", os.ModePerm)
+}
+
+/**
+ * UploadFile
+ * 功能:上传文件
+ * @param userID 上传者ID
+ * @param fileName 文件名
+ * @param fileType 文件类型(image/video)
+ * @param fileSize 文件大小
+ * @param fileData 文件数据
+ * @returns 附件信息和错误
+ */
+func (s *AttachmentService) UploadFile(userID, fileName, fileType string, fileSize int64, fileData io.Reader) (*model.Attachment, error) {
+ // 验证文件类型
+ if fileType != "image" && fileType != "video" {
+ return nil, errors.New("文件类型必须是image或video")
+ }
+
+ // 验证文件大小
+ if fileType == "image" && fileSize > model.MaxImageSize {
+ return nil, fmt.Errorf("图片大小不能超过%dMB", model.MaxImageSize/(1024*1024))
+ }
+ if fileType == "video" && fileSize > model.MaxVideoSize {
+ return nil, fmt.Errorf("视频大小不能超过%dMB", model.MaxVideoSize/(1024*1024))
+ }
+
+ // 验证文件扩展名
+ ext := strings.ToLower(filepath.Ext(fileName))
+ allowedExts := s.getAllowedExtensions(fileType)
+ if !contains(allowedExts, ext) {
+ return nil, fmt.Errorf("不支持的文件类型,允许的类型: %v", allowedExts)
+ }
+
+ // 生成唯一文件名
+ timestamp := time.Now().UnixNano()
+ randomStr := fmt.Sprintf("%d", timestamp%1000000)
+ newFileName := fmt.Sprintf("%d_%s%s", timestamp, randomStr, ext)
+
+ // 确定保存路径
+ var saveDir string
+ if fileType == "image" {
+ saveDir = "./uploads/images"
+ } else {
+ saveDir = "./uploads/videos"
+ }
+
+ filePath := filepath.Join(saveDir, newFileName)
+
+ // 保存文件
+ file, err := os.Create(filePath)
+ if err != nil {
+ return nil, fmt.Errorf("创建文件失败: %v", err)
+ }
+ defer file.Close()
+
+ _, err = io.Copy(file, fileData)
+ if err != nil {
+ os.Remove(filePath) // 删除失败的文件
+ return nil, fmt.Errorf("保存文件失败: %v", err)
+ }
+
+ // 生成访问URL
+ fileURL := fmt.Sprintf("/uploads/%s/%s", fileType+"s", newFileName)
+
+ // 获取MIME类型
+ mimeType := mime.TypeByExtension(ext)
+ if mimeType == "" {
+ mimeType = "application/octet-stream"
+ }
+
+ // 创建附件记录
+ attachment := model.Attachment{
+ UploaderID: userID,
+ FileName: fileName,
+ FileType: fileType,
+ FileSize: fileSize,
+ FilePath: filePath,
+ FileURL: fileURL,
+ MimeType: mimeType,
+ }
+
+ if err := s.DB.Create(&attachment).Error; err != nil {
+ os.Remove(filePath) // 删除文件
+ return nil, fmt.Errorf("保存附件记录失败: %v", err)
+ }
+
+ return &attachment, nil
+}
+
+/**
+ * GetAttachment
+ * 功能:获取附件信息
+ */
+func (s *AttachmentService) GetAttachment(attachmentID uint) (*model.Attachment, error) {
+ var attachment model.Attachment
+ result := s.DB.First(&attachment, attachmentID)
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ return &attachment, nil
+}
+
+/**
+ * DeleteAttachment
+ * 功能:删除附件(验证上传者权限)
+ */
+func (s *AttachmentService) DeleteAttachment(attachmentID uint, userID string) error {
+ var attachment model.Attachment
+ if err := s.DB.First(&attachment, attachmentID).Error; err != nil {
+ return err
+ }
+
+ // 验证权限
+ if attachment.UploaderID != userID {
+ return errors.New("无权删除此附件")
+ }
+
+ // 删除文件
+ if _, err := os.Stat(attachment.FilePath); err == nil {
+ os.Remove(attachment.FilePath)
+ }
+
+ // 删除记录
+ return s.DB.Delete(&attachment).Error
+}
+
+/**
+ * GetUserAttachments
+ * 功能:获取用户附件列表
+ */
+func (s *AttachmentService) GetUserAttachments(userID, fileType string, page, pageSize int) ([]model.Attachment, int64, error) {
+ var attachments []model.Attachment
+ var total int64
+
+ query := s.DB.Where("uploader_id = ?", userID)
+ if fileType != "" {
+ query = query.Where("file_type = ?", fileType)
+ }
+
+ // 获取总数
+ query.Model(&model.Attachment{}).Count(&total)
+
+ // 分页查询
+ offset := (page - 1) * pageSize
+ result := query.Order("created_at DESC").
+ Offset(offset).
+ Limit(pageSize).
+ Find(&attachments)
+
+ return attachments, total, result.Error
+}
+
+// 辅助函数:获取允许的文件扩展名
+func (s *AttachmentService) getAllowedExtensions(fileType string) []string {
+ if fileType == "image" {
+ return []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
+ }
+ return []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv"}
+}
+
+// 辅助函数:检查字符串是否在切片中
+func contains(slice []string, item string) bool {
+ for _, s := range slice {
+ if s == item {
+ return true
+ }
+ }
+ return false
+}
+
diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go
new file mode 100644
index 0000000..29fe0ca
--- /dev/null
+++ b/internal/service/auth_service.go
@@ -0,0 +1,235 @@
+/**
+ * package service
+ * 作用:认证服务,处理登录、注册、验证码等功能
+ */
+package service
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "math/big"
+ "strings"
+ "time"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/utils"
+
+ "github.com/go-redis/redis/v8"
+ "gorm.io/gorm"
+)
+
+// AuthService 认证服务结构体
+type AuthService struct {
+ DB *gorm.DB
+ Redis *redis.Client
+}
+
+// AuthSvc 全局单例
+var AuthSvc *AuthService
+
+/**
+ * InitAuthService
+ * 功能:初始化认证服务
+ */
+func InitAuthService(db *gorm.DB, rdb *redis.Client) {
+ AuthSvc = &AuthService{DB: db, Redis: rdb}
+}
+
+/**
+ * Login
+ * 功能:用户登录验证
+ * @param account 账号(邮箱或手机号)
+ * @param password 密码
+ * @returns 用户信息和错误
+ */
+func (s *AuthService) Login(account, password string) (*model.User, error) {
+ var user model.User
+
+ // 根据邮箱或手机号查询用户
+ result := s.DB.Where("email = ? OR phone = ?", account, account).First(&user)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return nil, errors.New("用户不存在")
+ }
+ return nil, result.Error
+ }
+
+ // 验证密码
+ if !utils.CheckPassword(password, user.Password) {
+ return nil, errors.New("密码错误")
+ }
+
+ return &user, nil
+}
+
+/**
+ * Register
+ * 功能:用户注册
+ * @param req 注册请求
+ * @returns 用户信息和错误
+ */
+func (s *AuthService) Register(req *model.RegisterReq) (*model.User, error) {
+ // 验证密码一致性
+ if req.Password != req.ConfirmPassword {
+ return nil, errors.New("两次密码输入不一致")
+ }
+
+ // 检查邮箱是否已存在
+ var existingUser model.User
+ if err := s.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
+ return nil, errors.New("邮箱已被注册")
+ }
+
+ // 检查手机号是否已存在
+ if err := s.DB.Where("phone = ?", req.Phone).First(&existingUser).Error; err == nil {
+ return nil, errors.New("手机号已被注册")
+ }
+
+ // 加密密码
+ hashedPassword, err := utils.HashPassword(req.Password)
+ if err != nil {
+ return nil, fmt.Errorf("密码加密失败: %v", err)
+ }
+
+ // 生成用户ID(使用时间戳+随机数)
+ userID := fmt.Sprintf("%d%s", time.Now().UnixNano(), generateRandomString(6))
+
+ // 生成默认名称(使用邮箱前缀)
+ defaultName := req.Email
+ if atIndex := strings.Index(req.Email, "@"); atIndex > 0 {
+ defaultName = req.Email[:atIndex]
+ }
+ if len(defaultName) > 20 {
+ defaultName = defaultName[:20]
+ }
+
+ // 创建用户
+ user := model.User{
+ ID: userID,
+ Email: req.Email,
+ Phone: req.Phone,
+ Password: hashedPassword,
+ Name: defaultName,
+ Avatar: generateAvatar(userID),
+ Desc: "",
+ Region: "",
+ }
+
+ if err := s.DB.Create(&user).Error; err != nil {
+ return nil, fmt.Errorf("创建用户失败: %v", err)
+ }
+
+ // 清除密码字段
+ user.Password = ""
+
+ return &user, nil
+}
+
+/**
+ * SendEmailCode
+ * 功能:发送邮箱验证码(模拟)
+ * @param email 邮箱
+ * @returns 验证码和错误
+ */
+func (s *AuthService) SendEmailCode(email string) (string, error) {
+ // 生成6位验证码
+ code := generateCode(6)
+
+ // 保存验证码到数据库(5分钟过期)
+ vc := model.VerificationCode{
+ Target: email,
+ Code: code,
+ Type: "email",
+ ExpiresAt: time.Now().Add(5 * time.Minute),
+ }
+
+ if err := s.DB.Create(&vc).Error; err != nil {
+ return "", fmt.Errorf("保存验证码失败: %v", err)
+ }
+
+ // 模拟发送(实际应调用邮件服务)
+ // 这里直接返回验证码,生产环境应通过邮件发送
+ return code, nil
+}
+
+/**
+ * SendSmsCode
+ * 功能:发送短信验证码(模拟)
+ * @param phone 手机号
+ * @returns 验证码和错误
+ */
+func (s *AuthService) SendSmsCode(phone string) (string, error) {
+ // 生成6位验证码
+ code := generateCode(6)
+
+ // 保存验证码到数据库(5分钟过期)
+ vc := model.VerificationCode{
+ Target: phone,
+ Code: code,
+ Type: "sms",
+ ExpiresAt: time.Now().Add(5 * time.Minute),
+ }
+
+ if err := s.DB.Create(&vc).Error; err != nil {
+ return "", fmt.Errorf("保存验证码失败: %v", err)
+ }
+
+ // 模拟发送(实际应调用短信服务)
+ // 这里直接返回验证码,生产环境应通过短信发送
+ return code, nil
+}
+
+/**
+ * VerifyCode
+ * 功能:验证验证码
+ * @param target 邮箱或手机号
+ * @param code 验证码
+ * @param codeType 类型(email/sms)
+ * @returns 是否有效
+ */
+func (s *AuthService) VerifyCode(target, code, codeType string) (bool, error) {
+ var vc model.VerificationCode
+
+ // 查询验证码
+ result := s.DB.Where("target = ? AND code = ? AND type = ? AND expires_at > ?",
+ target, code, codeType, time.Now()).
+ Order("created_at DESC").
+ First(&vc)
+
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return false, errors.New("验证码无效或已过期")
+ }
+ return false, result.Error
+ }
+
+ return true, nil
+}
+
+// 辅助函数:生成随机字符串
+func generateRandomString(length int) string {
+ b := make([]byte, length)
+ rand.Read(b)
+ return hex.EncodeToString(b)[:length]
+}
+
+// 辅助函数:生成验证码
+func generateCode(length int) string {
+ code := ""
+ for i := 0; i < length; i++ {
+ n, _ := rand.Int(rand.Reader, big.NewInt(10))
+ code += n.String()
+ }
+ return code
+}
+
+// 辅助函数:生成默认头像
+func generateAvatar(userID string) string {
+ // 简单实现:使用用户ID的第一个字符
+ if len(userID) > 0 {
+ return string(userID[0])
+ }
+ return "U"
+}
+
diff --git a/internal/service/contact_service.go b/internal/service/contact_service.go
new file mode 100644
index 0000000..e74e703
--- /dev/null
+++ b/internal/service/contact_service.go
@@ -0,0 +1,310 @@
+/**
+ * package service
+ * 作用:联系人管理服务
+ */
+package service
+
+import (
+ "errors"
+ "xk-websocket-v2/internal/model"
+
+ "gorm.io/gorm"
+)
+
+// ContactService 联系人服务结构体
+type ContactService struct {
+ DB *gorm.DB
+}
+
+// ContactSvc 全局单例
+var ContactSvc *ContactService
+
+/**
+ * InitContactService
+ * 功能:初始化联系人服务
+ */
+func InitContactService(db *gorm.DB) {
+ ContactSvc = &ContactService{DB: db}
+}
+
+/**
+ * SearchUsers
+ * 功能:搜索用户(按用户名、ID、邮箱、手机号)
+ */
+func (s *ContactService) SearchUsers(keyword string, limit int) ([]model.User, error) {
+ return UserSvc.SearchUsers(keyword, limit)
+}
+
+/**
+ * AddFriend
+ * 功能:发送好友申请
+ */
+func (s *ContactService) AddFriend(fromUserID, toUserID, message string) error {
+ // 检查是否已经是好友
+ var existingContact model.UserContact
+ result := s.DB.Where("user_id = ? AND contact_id = ?", fromUserID, toUserID).First(&existingContact)
+ if result.Error == nil {
+ return errors.New("已经是好友关系")
+ }
+
+ // 检查是否已有待处理的申请
+ var existingRequest model.FriendRequest
+ result = s.DB.Where("from_user_id = ? AND to_user_id = ? AND status = ?",
+ fromUserID, toUserID, "pending").First(&existingRequest)
+ if result.Error == nil {
+ return errors.New("已发送过好友申请")
+ }
+
+ // 创建好友申请
+ request := model.FriendRequest{
+ FromUserID: fromUserID,
+ ToUserID: toUserID,
+ Message: message,
+ Status: "pending",
+ }
+
+ return s.DB.Create(&request).Error
+}
+
+/**
+ * GetFriendRequests
+ * 功能:获取好友申请列表
+ */
+func (s *ContactService) GetFriendRequests(userID string) ([]model.FriendRequest, error) {
+ var requests []model.FriendRequest
+ result := s.DB.Where("to_user_id = ? AND status = ?", userID, "pending").
+ Order("created_at DESC").
+ Find(&requests)
+ return requests, result.Error
+}
+
+/**
+ * AcceptFriendRequest
+ * 功能:接受好友申请
+ */
+func (s *ContactService) AcceptFriendRequest(requestID uint, userID string) error {
+ // 查找申请
+ var request model.FriendRequest
+ if err := s.DB.First(&request, requestID).Error; err != nil {
+ return err
+ }
+
+ // 验证是否为接收者
+ if request.ToUserID != userID {
+ return errors.New("无权操作此申请")
+ }
+
+ // 开始事务
+ tx := s.DB.Begin()
+
+ // 更新申请状态
+ if err := tx.Model(&request).Update("status", "accepted").Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ // 创建双向好友关系
+ contact1 := model.UserContact{
+ UserID: request.FromUserID,
+ ContactID: request.ToUserID,
+ }
+ contact2 := model.UserContact{
+ UserID: request.ToUserID,
+ ContactID: request.FromUserID,
+ }
+
+ if err := tx.Create(&contact1).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+ if err := tx.Create(&contact2).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ return tx.Commit().Error
+}
+
+/**
+ * RejectFriendRequest
+ * 功能:拒绝好友申请
+ */
+func (s *ContactService) RejectFriendRequest(requestID uint, userID string) error {
+ var request model.FriendRequest
+ if err := s.DB.First(&request, requestID).Error; err != nil {
+ return err
+ }
+
+ if request.ToUserID != userID {
+ return errors.New("无权操作此申请")
+ }
+
+ return s.DB.Model(&request).Update("status", "rejected").Error
+}
+
+/**
+ * GetContacts
+ * 功能:获取好友列表
+ */
+func (s *ContactService) GetContacts(userID string) ([]model.UserContact, error) {
+ var contacts []model.UserContact
+ result := s.DB.Where("user_id = ?", userID).
+ Order("is_top DESC, last_chat_time DESC, created_at DESC").
+ Find(&contacts)
+ return contacts, result.Error
+}
+
+/**
+ * GetContactDetail
+ * 功能:获取好友详情
+ */
+func (s *ContactService) GetContactDetail(userID, contactID string) (*model.UserContact, error) {
+ var contact model.UserContact
+ result := s.DB.Where("user_id = ? AND contact_id = ?", userID, contactID).First(&contact)
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ return &contact, nil
+}
+
+/**
+ * UpdateContact
+ * 功能:更新好友信息(备注、分组等)
+ */
+func (s *ContactService) UpdateContact(userID, contactID string, updates map[string]interface{}) error {
+ return s.DB.Model(&model.UserContact{}).
+ Where("user_id = ? AND contact_id = ?", userID, contactID).
+ Updates(updates).Error
+}
+
+/**
+ * DeleteContact
+ * 功能:删除好友
+ */
+func (s *ContactService) DeleteContact(userID, contactID string) error {
+ // 删除双向好友关系
+ tx := s.DB.Begin()
+
+ if err := tx.Where("user_id = ? AND contact_id = ?", userID, contactID).Delete(&model.UserContact{}).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ if err := tx.Where("user_id = ? AND contact_id = ?", contactID, userID).Delete(&model.UserContact{}).Error; err != nil {
+ tx.Rollback()
+ return err
+ }
+
+ return tx.Commit().Error
+}
+
+/**
+ * GetGroups
+ * 功能:获取分组列表
+ */
+func (s *ContactService) GetGroups(userID string) ([]model.ContactGroup, error) {
+ var groups []model.ContactGroup
+ result := s.DB.Where("user_id = ?", userID).
+ Order("sort_order ASC, created_at ASC").
+ Find(&groups)
+ return groups, result.Error
+}
+
+/**
+ * CreateGroup
+ * 功能:创建分组
+ */
+func (s *ContactService) CreateGroup(userID, groupName string) (*model.ContactGroup, error) {
+ // 获取当前最大排序值
+ var maxOrder int
+ s.DB.Model(&model.ContactGroup{}).
+ Where("user_id = ?", userID).
+ Select("COALESCE(MAX(sort_order), 0)").
+ Scan(&maxOrder)
+
+ group := model.ContactGroup{
+ UserID: userID,
+ GroupName: groupName,
+ SortOrder: maxOrder + 1,
+ }
+
+ if err := s.DB.Create(&group).Error; err != nil {
+ return nil, err
+ }
+
+ return &group, nil
+}
+
+/**
+ * UpdateGroup
+ * 功能:更新分组
+ */
+func (s *ContactService) UpdateGroup(groupID uint, userID string, updates map[string]interface{}) error {
+ return s.DB.Model(&model.ContactGroup{}).
+ Where("id = ? AND user_id = ?", groupID, userID).
+ Updates(updates).Error
+}
+
+/**
+ * DeleteGroup
+ * 功能:删除分组
+ */
+func (s *ContactService) DeleteGroup(groupID uint, userID string) error {
+ // 检查分组是否存在且属于该用户
+ var group model.ContactGroup
+ if err := s.DB.Where("id = ? AND user_id = ?", groupID, userID).First(&group).Error; err != nil {
+ return err
+ }
+
+ // 将该分组下的联系人移到默认分组(group_id = 0)
+ if err := s.DB.Model(&model.UserContact{}).
+ Where("user_id = ? AND group_id = ?", userID, groupID).
+ Update("group_id", 0).Error; err != nil {
+ return err
+ }
+
+ // 删除分组
+ return s.DB.Delete(&group).Error
+}
+
+/**
+ * GetContactsWithUserInfo
+ * 功能:获取好友列表(包含用户信息)
+ */
+func (s *ContactService) GetContactsWithUserInfo(userID string) ([]map[string]interface{}, error) {
+ var contacts []model.UserContact
+ if err := s.DB.Where("user_id = ?", userID).
+ Order("is_top DESC, last_chat_time DESC").
+ Find(&contacts).Error; err != nil {
+ return nil, err
+ }
+
+ var result []map[string]interface{}
+ for _, contact := range contacts {
+ // 获取联系人用户信息
+ var user model.User
+ if err := s.DB.Where("id = ?", contact.ContactID).First(&user).Error; err != nil {
+ continue
+ }
+ user.Password = ""
+
+ // 组合数据
+ item := map[string]interface{}{
+ "id": contact.ContactID,
+ "name": user.Name,
+ "avatar": user.Avatar,
+ "desc": user.Desc,
+ "remark_name": contact.RemarkName,
+ "group_id": contact.GroupID,
+ "is_top": contact.IsTop,
+ "is_muted": contact.IsMuted,
+ "last_chat_time": contact.LastChatTime,
+ "last_message": contact.LastMessage,
+ "unread_count": contact.UnreadCount,
+ }
+ result = append(result, item)
+ }
+
+ return result, nil
+}
+
diff --git a/internal/service/login_log_service.go b/internal/service/login_log_service.go
new file mode 100644
index 0000000..5734e3f
--- /dev/null
+++ b/internal/service/login_log_service.go
@@ -0,0 +1,56 @@
+/**
+ * package service
+ * 作用:登录日志服务
+ */
+package service
+
+import (
+ "time"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/utils"
+
+ "gorm.io/gorm"
+)
+
+// LoginLogService 登录日志服务
+type LoginLogService struct {
+ DB *gorm.DB
+}
+
+// LoginLogSvc 全局单例
+var LoginLogSvc *LoginLogService
+
+/**
+ * InitLoginLogService
+ * 功能:初始化登录日志服务
+ */
+func InitLoginLogService(db *gorm.DB) {
+ LoginLogSvc = &LoginLogService{DB: db}
+}
+
+/**
+ * LogLogin
+ * 功能:记录登录日志
+ * @param account 登录账号
+ * @param userID 用户ID(失败为"0")
+ * @param ip IP地址
+ * @param success 是否成功
+ */
+func (s *LoginLogService) LogLogin(account, userID, ip string, success bool) {
+ // 获取IP归属地
+ location := utils.GetIPLocation(ip)
+
+ // 创建登录日志
+ log := model.LoginLog{
+ Account: account,
+ UserID: userID,
+ Location: location,
+ IP: ip,
+ Success: success,
+ LoginTime: time.Now(),
+ }
+
+ // 异步写入数据库
+ go s.DB.Create(&log)
+}
+
diff --git a/internal/service/room_service.go b/internal/service/room_service.go
new file mode 100644
index 0000000..cd0c8a3
--- /dev/null
+++ b/internal/service/room_service.go
@@ -0,0 +1,148 @@
+/**
+ * package service
+ * 作用:房间管理服务
+ */
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/utils"
+
+ "gorm.io/gorm"
+)
+
+// RoomService 房间服务结构体
+type RoomService struct {
+ DB *gorm.DB
+}
+
+// RoomSvc 全局单例
+var RoomSvc *RoomService
+
+/**
+ * InitRoomService
+ * 功能:初始化房间服务
+ */
+func InitRoomService(db *gorm.DB) {
+ RoomSvc = &RoomService{DB: db}
+}
+
+/**
+ * GenerateP2PRoomID
+ * 功能:生成点对点聊天房间ID
+ * 规则:将两个用户ID按字母序排序后,用下划线连接
+ */
+func GenerateP2PRoomID(userID1, userID2 string) string {
+ if userID1 < userID2 {
+ return fmt.Sprintf("%s_%s", userID1, userID2)
+ }
+ return fmt.Sprintf("%s_%s", userID2, userID1)
+}
+
+/**
+ * GenerateGroupRoomID
+ * 功能:生成群聊房间ID
+ * 规则:使用雪花ID生成全局唯一ID
+ */
+func GenerateGroupRoomID() string {
+ id, err := utils.NextID()
+ if err != nil {
+ // 如果雪花ID生成失败,使用备用方案
+ return fmt.Sprintf("group_%d", time.Now().UnixNano())
+ }
+ return fmt.Sprintf("group_%d", id)
+}
+
+/**
+ * CreateRoom
+ * 功能:创建房间
+ */
+func (s *RoomService) CreateRoom(roomType string, members []string, creatorID string) (*model.ChatRoom, error) {
+ var roomID string
+ if roomType == "p2p" {
+ if len(members) != 2 {
+ return nil, fmt.Errorf("点对点房间需要2个成员")
+ }
+ roomID = GenerateP2PRoomID(members[0], members[1])
+ } else {
+ roomID = GenerateGroupRoomID()
+ }
+
+ // 检查房间是否已存在
+ var existingRoom model.ChatRoom
+ if err := s.DB.Where("room_id = ?", roomID).First(&existingRoom).Error; err == nil {
+ return &existingRoom, nil
+ }
+
+ // 序列化成员列表
+ membersJSON, err := json.Marshal(members)
+ if err != nil {
+ return nil, err
+ }
+
+ room := model.ChatRoom{
+ RoomID: roomID,
+ RoomType: roomType,
+ Members: string(membersJSON),
+ CreatorID: creatorID,
+ }
+
+ if roomType == "group" {
+ room.RoomName = "群聊"
+ }
+
+ if err := s.DB.Create(&room).Error; err != nil {
+ return nil, err
+ }
+
+ return &room, nil
+}
+
+/**
+ * GetRoom
+ * 功能:获取房间信息
+ */
+func (s *RoomService) GetRoom(roomID string) (*model.ChatRoom, error) {
+ var room model.ChatRoom
+ result := s.DB.Where("room_id = ?", roomID).First(&room)
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ return &room, nil
+}
+
+/**
+ * GetOrCreateP2PRoom
+ * 功能:获取或创建点对点房间
+ */
+func (s *RoomService) GetOrCreateP2PRoom(userID1, userID2 string) (*model.ChatRoom, error) {
+ roomID := GenerateP2PRoomID(userID1, userID2)
+
+ // 尝试获取现有房间
+ var room model.ChatRoom
+ if err := s.DB.Where("room_id = ?", roomID).First(&room).Error; err == nil {
+ return &room, nil
+ }
+
+ // 创建新房间
+ members := []string{userID1, userID2}
+ return s.CreateRoom("p2p", members, userID1)
+}
+
+/**
+ * UpdateRoomLastMessage
+ * 功能:更新房间最后消息
+ */
+func (s *RoomService) UpdateRoomLastMessage(roomID, message string) error {
+ now := time.Now()
+ return s.DB.Model(&model.ChatRoom{}).
+ Where("room_id = ?", roomID).
+ Updates(map[string]interface{}{
+ "last_message": message,
+ "last_message_time": now,
+ }).Error
+}
+
diff --git a/internal/service/user_service.go b/internal/service/user_service.go
new file mode 100644
index 0000000..c1604bd
--- /dev/null
+++ b/internal/service/user_service.go
@@ -0,0 +1,165 @@
+/**
+ * package service
+ * 作用:用户管理服务
+ */
+package service
+
+import (
+ "errors"
+ "xk-websocket-v2/internal/model"
+ "xk-websocket-v2/internal/utils"
+
+ "gorm.io/gorm"
+)
+
+// UserService 用户服务结构体
+type UserService struct {
+ DB *gorm.DB
+}
+
+// UserSvc 全局单例
+var UserSvc *UserService
+
+/**
+ * InitUserService
+ * 功能:初始化用户服务
+ */
+func InitUserService(db *gorm.DB) {
+ UserSvc = &UserService{DB: db}
+}
+
+/**
+ * GetUserByID
+ * 功能:根据ID获取用户
+ */
+func (s *UserService) GetUserByID(userID string) (*model.User, error) {
+ var user model.User
+ result := s.DB.Where("id = ?", userID).First(&user)
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ user.Password = "" // 清除密码
+ return &user, nil
+}
+
+/**
+ * GetUserByEmail
+ * 功能:根据邮箱获取用户
+ */
+func (s *UserService) GetUserByEmail(email string) (*model.User, error) {
+ var user model.User
+ result := s.DB.Where("email = ?", email).First(&user)
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ user.Password = "" // 清除密码
+ return &user, nil
+}
+
+/**
+ * GetUserByPhone
+ * 功能:根据手机号获取用户
+ */
+func (s *UserService) GetUserByPhone(phone string) (*model.User, error) {
+ var user model.User
+ result := s.DB.Where("phone = ?", phone).First(&user)
+ if result.Error != nil {
+ return nil, result.Error
+ }
+ user.Password = "" // 清除密码
+ return &user, nil
+}
+
+/**
+ * CreateUser
+ * 功能:创建用户
+ */
+func (s *UserService) CreateUser(user *model.User) error {
+ // 如果提供了密码,加密密码
+ if user.Password != "" {
+ hashedPassword, err := utils.HashPassword(user.Password)
+ if err != nil {
+ return err
+ }
+ user.Password = hashedPassword
+ }
+ return s.DB.Create(user).Error
+}
+
+/**
+ * UpdateUser
+ * 功能:更新用户信息
+ */
+func (s *UserService) UpdateUser(userID string, updates map[string]interface{}) error {
+ // 如果更新密码,需要加密
+ if password, ok := updates["password"].(string); ok && password != "" {
+ hashedPassword, err := utils.HashPassword(password)
+ if err != nil {
+ return err
+ }
+ updates["password"] = hashedPassword
+ }
+ return s.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
+}
+
+/**
+ * DeleteUser
+ * 功能:删除用户
+ */
+func (s *UserService) DeleteUser(userID string) error {
+ return s.DB.Where("id = ?", userID).Delete(&model.User{}).Error
+}
+
+/**
+ * GetUserList
+ * 功能:获取用户列表(分页)
+ */
+func (s *UserService) GetUserList(page, pageSize int) ([]model.User, int64, error) {
+ var users []model.User
+ var total int64
+
+ // 获取总数
+ s.DB.Model(&model.User{}).Count(&total)
+
+ // 分页查询
+ offset := (page - 1) * pageSize
+ result := s.DB.Offset(offset).Limit(pageSize).Find(&users)
+ if result.Error != nil {
+ return nil, 0, result.Error
+ }
+
+ // 清除所有用户的密码
+ for i := range users {
+ users[i].Password = ""
+ }
+
+ return users, total, nil
+}
+
+/**
+ * SearchUsers
+ * 功能:搜索用户(按用户名、ID、邮箱、手机号)
+ */
+func (s *UserService) SearchUsers(keyword string, limit int) ([]model.User, error) {
+ var users []model.User
+
+ query := s.DB.Where("name LIKE ? OR id LIKE ? OR email LIKE ? OR phone LIKE ?",
+ "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
+
+ if limit > 0 {
+ query = query.Limit(limit)
+ }
+
+ result := query.Find(&users)
+ if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ return nil, result.Error
+ }
+
+ // 清除所有用户的密码
+ for i := range users {
+ users[i].Password = ""
+ }
+
+ return users, nil
+}
+
diff --git a/internal/utils/ip_location.go b/internal/utils/ip_location.go
new file mode 100644
index 0000000..9950068
--- /dev/null
+++ b/internal/utils/ip_location.go
@@ -0,0 +1,163 @@
+/**
+ * package utils
+ * 作用:IP归属地查询工具
+ * 说明:使用第三方API查询IP归属地信息
+ */
+package utils
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+)
+
+// IPLocationInfo IP归属地信息
+type IPLocationInfo struct {
+ Country string `json:"country"` // 国家
+ Region string `json:"region"` // 省份
+ City string `json:"city"` // 城市
+ ISP string `json:"isp"` // 运营商
+ FullLocation string `json:"full_location"` // 完整归属地
+}
+
+// 缓存结构(简单内存缓存)
+var (
+ ipCache = make(map[string]*IPLocationInfo)
+ cacheTimeout = 24 * time.Hour
+)
+
+/**
+ * GetIPLocation
+ * 功能:获取IP归属地信息
+ * @param ip IP地址
+ * @returns 归属地信息字符串
+ */
+func GetIPLocation(ip string) string {
+ // 排除本地IP
+ if isLocalIP(ip) {
+ return "本地"
+ }
+
+ // 检查缓存
+ if info, ok := ipCache[ip]; ok {
+ return info.FullLocation
+ }
+
+ // 查询IP归属地
+ info := queryIPLocation(ip)
+ if info != nil {
+ // 构建完整归属地字符串
+ parts := []string{}
+ if info.Country != "" {
+ parts = append(parts, info.Country)
+ }
+ if info.Region != "" {
+ parts = append(parts, info.Region)
+ }
+ if info.City != "" {
+ parts = append(parts, info.City)
+ }
+ if info.ISP != "" {
+ parts = append(parts, info.ISP)
+ }
+
+ if len(parts) > 0 {
+ info.FullLocation = strings.Join(parts, " ")
+ } else {
+ info.FullLocation = "未知"
+ }
+
+ // 存入缓存
+ ipCache[ip] = info
+ return info.FullLocation
+ }
+
+ return "未知"
+}
+
+/**
+ * isLocalIP
+ * 功能:判断是否为本地IP
+ */
+func isLocalIP(ip string) bool {
+ // 本地回环地址
+ if ip == "127.0.0.1" || ip == "localhost" || ip == "::1" {
+ return true
+ }
+
+ // 内网地址
+ if strings.HasPrefix(ip, "192.168.") ||
+ strings.HasPrefix(ip, "10.") ||
+ strings.HasPrefix(ip, "172.16.") ||
+ strings.HasPrefix(ip, "172.17.") ||
+ strings.HasPrefix(ip, "172.18.") ||
+ strings.HasPrefix(ip, "172.19.") ||
+ strings.HasPrefix(ip, "172.20.") ||
+ strings.HasPrefix(ip, "172.21.") ||
+ strings.HasPrefix(ip, "172.22.") ||
+ strings.HasPrefix(ip, "172.23.") ||
+ strings.HasPrefix(ip, "172.24.") ||
+ strings.HasPrefix(ip, "172.25.") ||
+ strings.HasPrefix(ip, "172.26.") ||
+ strings.HasPrefix(ip, "172.27.") ||
+ strings.HasPrefix(ip, "172.28.") ||
+ strings.HasPrefix(ip, "172.29.") ||
+ strings.HasPrefix(ip, "172.30.") ||
+ strings.HasPrefix(ip, "172.31.") {
+ return true
+ }
+
+ return false
+}
+
+/**
+ * queryIPLocation
+ * 功能:查询IP归属地(使用ip-api.com免费API)
+ */
+func queryIPLocation(ip string) *IPLocationInfo {
+ // 使用ip-api.com免费API(限制:每分钟45次请求)
+ url := fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN&fields=status,message,country,regionName,city,isp", ip)
+
+ client := &http.Client{
+ Timeout: 3 * time.Second,
+ }
+
+ resp, err := client.Get(url)
+ if err != nil {
+ return nil
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil
+ }
+
+ var result struct {
+ Status string `json:"status"`
+ Message string `json:"message"`
+ Country string `json:"country"`
+ Region string `json:"regionName"`
+ City string `json:"city"`
+ ISP string `json:"isp"`
+ }
+
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil
+ }
+
+ if result.Status != "success" {
+ return nil
+ }
+
+ return &IPLocationInfo{
+ Country: result.Country,
+ Region: result.Region,
+ City: result.City,
+ ISP: result.ISP,
+ }
+}
+
diff --git a/internal/utils/jwt.go b/internal/utils/jwt.go
new file mode 100644
index 0000000..348be8b
--- /dev/null
+++ b/internal/utils/jwt.go
@@ -0,0 +1,166 @@
+/**
+ * package utils
+ *
+ * JWT Token生成和验证工具包
+ *
+ * 功能概述:
+ * 1. 生成JWT Token(包含用户ID和过期时间)
+ * 2. 解析JWT Token(验证签名和过期时间)
+ * 3. 验证Token有效性(提取用户ID)
+ *
+ * 使用场景:
+ * - 用户登录后生成Token
+ * - API请求时验证Token
+ * - 从Token中提取用户信息
+ */
+package utils
+
+import (
+ "errors"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ "github.com/spf13/viper"
+)
+
+// jwtSecret JWT签名密钥(从配置文件读取)
+var jwtSecret []byte
+
+/**
+ * init
+ *
+ * 功能:初始化JWT签名密钥
+ *
+ * 步骤:
+ * 1. 从配置文件读取JWT密钥
+ * 2. 如果配置文件中没有,使用默认密钥(仅用于开发环境)
+ * 3. 将密钥转换为字节数组存储
+ *
+ * 注意:生产环境必须使用配置文件中的强随机密钥
+ */
+func init() {
+ secret := viper.GetString("jwt.secret")
+ if secret == "" {
+ secret = "xk-websocket-secret-key-2025" // 默认密钥,生产环境应使用配置
+ }
+ jwtSecret = []byte(secret)
+}
+
+/**
+ * Claims
+ *
+ * JWT Token的载荷结构
+ *
+ * 字段说明:
+ * - UserID: 用户ID(业务数据)
+ * - RegisteredClaims: JWT标准声明(过期时间、签发时间等)
+ */
+type Claims struct {
+ UserID string `json:"user_id"` // 用户ID
+ jwt.RegisteredClaims // JWT标准声明
+}
+
+/**
+ * GenerateToken
+ *
+ * 功能:生成JWT Token
+ *
+ * 步骤:
+ * 1. 设置Token过期时间(默认7天)
+ * 2. 创建Claims对象,包含用户ID和标准声明
+ * 3. 使用HS256算法签名Token
+ * 4. 返回Token字符串
+ *
+ * @param userID 用户ID
+ * @returns token字符串和错误
+ */
+func GenerateToken(userID string) (string, error) {
+ // 步骤1: 设置Token过期时间(7天后过期)
+ expirationTime := time.Now().Add(7 * 24 * time.Hour)
+
+ // 步骤2: 创建Claims对象,包含用户ID和标准声明
+ claims := &Claims{
+ UserID: userID, // 业务数据:用户ID
+ RegisteredClaims: jwt.RegisteredClaims{
+ ExpiresAt: jwt.NewNumericDate(expirationTime), // 过期时间
+ IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
+ NotBefore: jwt.NewNumericDate(time.Now()), // 生效时间(立即生效)
+ },
+ }
+
+ // 步骤3: 使用HS256算法创建Token并签名
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenString, err := token.SignedString(jwtSecret)
+ if err != nil {
+ return "", err
+ }
+
+ // 步骤4: 返回Token字符串
+ return tokenString, nil
+}
+
+/**
+ * ParseToken
+ *
+ * 功能:解析JWT Token
+ *
+ * 步骤:
+ * 1. 创建空的Claims对象
+ * 2. 使用密钥解析Token并验证签名
+ * 3. 检查Token是否有效(签名正确、未过期)
+ * 4. 返回Claims对象
+ *
+ * @param tokenString token字符串
+ * @returns Claims和错误
+ */
+func ParseToken(tokenString string) (*Claims, error) {
+ // 步骤1: 创建空的Claims对象
+ claims := &Claims{}
+
+ // 步骤2: 解析Token并验证签名
+ // 使用密钥验证Token的签名是否有效
+ token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
+ return jwtSecret, nil // 返回签名密钥
+ })
+
+ if err != nil {
+ return nil, err // 解析失败(签名错误、格式错误等)
+ }
+
+ // 步骤3: 检查Token是否有效(签名正确、未过期)
+ if !token.Valid {
+ return nil, errors.New("invalid token")
+ }
+
+ // 步骤4: 返回解析后的Claims对象
+ return claims, nil
+}
+
+/**
+ * ValidateToken
+ *
+ * 功能:验证Token有效性并提取用户ID
+ *
+ * 步骤:
+ * 1. 调用ParseToken解析Token
+ * 2. 如果解析成功,从Claims中提取用户ID
+ * 3. 返回用户ID
+ *
+ * 使用场景:
+ * - 中间件中验证Token
+ * - API处理器中获取当前用户ID
+ *
+ * @param tokenString token字符串
+ * @returns 用户ID和错误
+ */
+func ValidateToken(tokenString string) (string, error) {
+ // 步骤1: 解析Token
+ claims, err := ParseToken(tokenString)
+ if err != nil {
+ return "", err // Token无效或已过期
+ }
+
+ // 步骤2: 从Claims中提取用户ID
+ return claims.UserID, nil
+}
+
diff --git a/internal/utils/password.go b/internal/utils/password.go
new file mode 100644
index 0000000..eb86e6e
--- /dev/null
+++ b/internal/utils/password.go
@@ -0,0 +1,82 @@
+/**
+ * package utils
+ *
+ * 密码加密和验证工具包
+ *
+ * 功能概述:
+ * 1. 使用bcrypt算法加密密码(单向哈希,不可逆)
+ * 2. 验证明文密码与加密密码是否匹配
+ *
+ * 安全特性:
+ * - 使用bcrypt算法,自动加盐
+ * - 计算成本可调,防止暴力破解
+ * - 相同密码每次加密结果不同(因为盐值随机)
+ */
+package utils
+
+import "golang.org/x/crypto/bcrypt"
+
+/**
+ * HashPassword
+ *
+ * 功能:使用bcrypt算法加密密码
+ *
+ * 步骤:
+ * 1. 将明文密码转换为字节数组
+ * 2. 使用bcrypt算法生成哈希值(自动加盐)
+ * 3. 将哈希值转换为字符串返回
+ *
+ * 特点:
+ * - 每次加密结果不同(因为盐值随机)
+ * - 使用默认计算成本(10轮)
+ * - 单向加密,不可逆
+ *
+ * 使用场景:
+ * - 用户注册时加密密码
+ * - 用户修改密码时加密新密码
+ *
+ * @param password 明文密码
+ * @returns 加密后的密码哈希字符串和错误
+ */
+func HashPassword(password string) (string, error) {
+ // 步骤1-2: 使用bcrypt算法生成密码哈希
+ // bcrypt.DefaultCost = 10,表示进行2^10=1024轮哈希计算
+ bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ return "", err
+ }
+
+ // 步骤3: 将哈希值转换为字符串返回
+ return string(bytes), nil
+}
+
+/**
+ * CheckPassword
+ *
+ * 功能:验证明文密码与加密密码是否匹配
+ *
+ * 步骤:
+ * 1. 从加密密码中提取盐值
+ * 2. 使用相同的盐值对明文密码进行哈希
+ * 3. 比较两个哈希值是否相同
+ *
+ * 特点:
+ * - 即使密码相同,每次加密的哈希值也不同
+ * - 但可以通过CompareHashAndPassword正确验证
+ * - 验证过程是安全的,不会泄露密码信息
+ *
+ * 使用场景:
+ * - 用户登录时验证密码
+ * - 修改密码时验证旧密码
+ *
+ * @param password 明文密码
+ * @param hash 加密后的密码哈希
+ * @returns 是否匹配(true=匹配,false=不匹配)
+ */
+func CheckPassword(password, hash string) bool {
+ // 步骤1-3: 比较明文密码的哈希值与存储的哈希值
+ // 如果匹配,err为nil,返回true;否则返回false
+ err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
+ return err == nil
+}
+
diff --git a/internal/utils/response.go b/internal/utils/response.go
new file mode 100644
index 0000000..50e3db5
--- /dev/null
+++ b/internal/utils/response.go
@@ -0,0 +1,172 @@
+/**
+ * package utils
+ * 作用:提供统一的API响应工具函数
+ * 说明:所有响应统一返回HTTP 200状态码,错误通过code字段标识
+ */
+package utils
+
+import (
+ "net/http"
+ "strconv"
+ "time"
+ "xk-websocket-v2/internal/model"
+
+ "github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
+)
+
+// 业务状态码常量
+const (
+ CodeSuccess = 0 // 成功
+ CodeBadRequest = 400 // 参数错误
+ CodeUnauthorized = 401 // 未认证
+ CodeForbidden = 403 // 无权限
+ CodeNotFound = 404 // 资源不存在
+ CodeInternalError = 500 // 服务器错误
+)
+
+// 响应类型常量
+const (
+ TypeSuccess = "success"
+ TypeError = "error"
+)
+
+/**
+ * getECS
+ * 作用:获取服务器标识
+ */
+func getECS() string {
+ ecs := viper.GetString("server.ecs")
+ if ecs == "" {
+ return "localhost"
+ }
+ return ecs
+}
+
+/**
+ * formatDuration
+ * 作用:格式化响应时间为 "XX ms" 格式
+ */
+func formatDuration(d time.Duration) string {
+ ms := d.Milliseconds()
+ return strconv.FormatInt(ms, 10) + " ms"
+}
+
+/**
+ * Response
+ * 作用:统一的响应函数,所有响应都通过此函数返回
+ * 说明:统一返回HTTP 200状态码
+ */
+func Response(c *gin.Context, code int, message string, result interface{}, responseType string) {
+ // 从Context获取请求开始时间
+ startTime, exists := c.Get("request_start_time")
+ var duration time.Duration
+ if exists {
+ duration = time.Since(startTime.(time.Time))
+ } else {
+ duration = 0
+ }
+
+ response := model.ApiResponse{
+ Code: code,
+ Message: message,
+ Result: result,
+ Type: responseType,
+ InterfaceInfo: model.InterfaceInfo{
+ ResultTime: formatDuration(duration),
+ Ecs: getECS(),
+ },
+ }
+
+ // 统一返回HTTP 200状态码
+ c.JSON(http.StatusOK, response)
+}
+
+/**
+ * Success
+ * 作用:成功响应(无数据)
+ */
+func Success(c *gin.Context, message string) {
+ if message == "" {
+ message = "操作成功"
+ }
+ Response(c, CodeSuccess, message, nil, TypeSuccess)
+}
+
+/**
+ * SuccessWithData
+ * 作用:成功响应(带数据)
+ */
+func SuccessWithData(c *gin.Context, data interface{}, message string) {
+ if message == "" {
+ message = "获取成功"
+ }
+ Response(c, CodeSuccess, message, data, TypeSuccess)
+}
+
+/**
+ * Error
+ * 作用:错误响应(自定义状态码和消息)
+ */
+func Error(c *gin.Context, code int, message string) {
+ if message == "" {
+ message = "操作失败"
+ }
+ Response(c, code, message, nil, TypeError)
+}
+
+/**
+ * BadRequest
+ * 作用:参数错误响应(code=400)
+ */
+func BadRequest(c *gin.Context, message string) {
+ if message == "" {
+ message = "参数错误"
+ }
+ Response(c, CodeBadRequest, message, nil, TypeError)
+}
+
+/**
+ * Unauthorized
+ * 作用:未认证响应(code=401)
+ */
+func Unauthorized(c *gin.Context, message string) {
+ if message == "" {
+ message = "未认证"
+ }
+ Response(c, CodeUnauthorized, message, nil, TypeError)
+}
+
+/**
+ * Forbidden
+ * 作用:无权限响应(code=403)
+ */
+func Forbidden(c *gin.Context, message string) {
+ if message == "" {
+ message = "无权限"
+ }
+ Response(c, CodeForbidden, message, nil, TypeError)
+}
+
+/**
+ * NotFound
+ * 作用:资源不存在响应(code=404)
+ */
+func NotFound(c *gin.Context, message string) {
+ if message == "" {
+ message = "资源不存在"
+ }
+ Response(c, CodeNotFound, message, nil, TypeError)
+}
+
+/**
+ * InternalError
+ * 作用:服务器错误响应(code=500)
+ */
+func InternalError(c *gin.Context, message string) {
+ if message == "" {
+ message = "服务器错误"
+ }
+ Response(c, CodeInternalError, message, nil, TypeError)
+}
+
diff --git a/internal/utils/snowflake.go b/internal/utils/snowflake.go
new file mode 100644
index 0000000..58146ad
--- /dev/null
+++ b/internal/utils/snowflake.go
@@ -0,0 +1,182 @@
+/**
+ * package utils
+ * 作用:雪花ID生成器,生成全局唯一的ID
+ * 说明:使用Twitter的雪花算法,生成64位整数ID
+ */
+package utils
+
+import (
+ "errors"
+ "sync"
+ "time"
+)
+
+const (
+ // 时间戳占用位数(41位,可以使用69年)
+ timestampBits = 41
+ // 数据中心ID占用位数(5位,最多32个数据中心)
+ datacenterIDBits = 5
+ // 机器ID占用位数(5位,每个数据中心最多32台机器)
+ machineIDBits = 5
+ // 序列号占用位数(12位,每毫秒最多4096个ID)
+ sequenceBits = 12
+
+ // 最大值
+ maxDatacenterID = -1 ^ (-1 << datacenterIDBits)
+ maxMachineID = -1 ^ (-1 << machineIDBits)
+ maxSequence = -1 ^ (-1 << sequenceBits)
+
+ // 位移
+ machineIDShift = sequenceBits
+ datacenterIDShift = sequenceBits + machineIDBits
+ timestampShift = sequenceBits + machineIDBits + datacenterIDBits
+
+ // 起始时间戳(2024-01-01 00:00:00)
+ epoch int64 = 1704067200000
+)
+
+// Snowflake 雪花ID生成器
+type Snowflake struct {
+ mutex sync.Mutex
+ datacenterID int64
+ machineID int64
+ sequence int64
+ lastStamp int64
+}
+
+var (
+ // 全局雪花ID生成器实例
+ globalSnowflake *Snowflake
+ once sync.Once
+)
+
+/**
+ * InitSnowflake
+ * 功能:初始化全局雪花ID生成器
+ * @param datacenterID 数据中心ID(0-31)
+ * @param machineID 机器ID(0-31)
+ */
+func InitSnowflake(datacenterID, machineID int64) error {
+ if datacenterID < 0 || datacenterID > maxDatacenterID {
+ return errors.New("datacenter ID must be between 0 and 31")
+ }
+ if machineID < 0 || machineID > maxMachineID {
+ return errors.New("machine ID must be between 0 and 31")
+ }
+
+ once.Do(func() {
+ globalSnowflake = &Snowflake{
+ datacenterID: datacenterID,
+ machineID: machineID,
+ sequence: 0,
+ lastStamp: -1,
+ }
+ })
+
+ return nil
+}
+
+/**
+ * NextID
+ * 功能:生成下一个ID
+ * @returns 64位整数ID
+ */
+func NextID() (int64, error) {
+ if globalSnowflake == nil {
+ // 默认使用datacenterID=1, machineID=1
+ if err := InitSnowflake(1, 1); err != nil {
+ return 0, err
+ }
+ }
+
+ return globalSnowflake.nextID()
+}
+
+/**
+ * nextID
+ * 功能:生成下一个ID(内部方法)
+ */
+func (s *Snowflake) nextID() (int64, error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ now := time.Now().UnixMilli()
+
+ // 如果当前时间小于上次时间,说明时钟回拨
+ if now < s.lastStamp {
+ return 0, errors.New("clock moved backwards")
+ }
+
+ // 如果是同一毫秒内
+ if now == s.lastStamp {
+ s.sequence = (s.sequence + 1) & maxSequence
+ // 序列号溢出,等待下一毫秒
+ if s.sequence == 0 {
+ now = s.waitNextMillis(s.lastStamp)
+ }
+ } else {
+ // 新的毫秒,序列号重置
+ s.sequence = 0
+ }
+
+ s.lastStamp = now
+
+ // 生成ID
+ id := ((now - epoch) << timestampShift) |
+ (s.datacenterID << datacenterIDShift) |
+ (s.machineID << machineIDShift) |
+ s.sequence
+
+ return id, nil
+}
+
+/**
+ * waitNextMillis
+ * 功能:等待下一毫秒
+ */
+func (s *Snowflake) waitNextMillis(lastStamp int64) int64 {
+ now := time.Now().UnixMilli()
+ for now <= lastStamp {
+ now = time.Now().UnixMilli()
+ }
+ return now
+}
+
+/**
+ * NextIDString
+ * 功能:生成下一个ID(字符串格式)
+ */
+func NextIDString() (string, error) {
+ id, err := NextID()
+ if err != nil {
+ return "", err
+ }
+ return int64ToString(id), nil
+}
+
+/**
+ * int64ToString
+ * 功能:将int64转换为字符串
+ */
+func int64ToString(id int64) string {
+ if id == 0 {
+ return "0"
+ }
+ negative := id < 0
+ if negative {
+ id = -id
+ }
+
+ var result []byte
+ for id > 0 {
+ result = append([]byte{byte('0' + id%10)}, result...)
+ id /= 10
+ }
+
+ if negative {
+ result = append([]byte{'-'}, result...)
+ }
+
+ return string(result)
+}
+
diff --git a/server.exe b/server.exe
new file mode 100644
index 0000000..0f8794e
Binary files /dev/null and b/server.exe differ
diff --git a/添加默认用户信息.sql b/添加默认用户信息.sql
new file mode 100644
index 0000000..967b63f
--- /dev/null
+++ b/添加默认用户信息.sql
@@ -0,0 +1,38 @@
+-- ==========================================
+-- IM系统 - 添加默认用户信息
+-- 说明:为用户表添加20个测试用户,ID从10001开始
+-- 所有用户默认密码:12345678
+-- ==========================================
+
+-- 插入20个测试用户
+INSERT INTO `users` (`id`, `email`, `phone`, `password`, `name`, `avatar`, `desc`, `region`, `created_at`, `updated_at`) VALUES
+('10001', 'user001@example.com', '13800138001', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '张三', '张', 'Golang 开发工程师', '北京', NOW(), NOW()),
+('10002', 'user002@example.com', '13800138002', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '李四', '李', '前端开发工程师', '上海', NOW(), NOW()),
+('10003', 'user003@example.com', '13800138003', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王五', '王', '产品经理', '深圳', NOW(), NOW()),
+('10004', 'user004@example.com', '13800138004', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '赵六', '赵', 'UI设计师', '广州', NOW(), NOW()),
+('10005', 'user005@example.com', '13800138005', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '钱七', '钱', '测试工程师', '杭州', NOW(), NOW()),
+('10006', 'user006@example.com', '13800138006', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '孙八', '孙', '运维工程师', '成都', NOW(), NOW()),
+('10007', 'user007@example.com', '13800138007', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '周九', '周', '数据分析师', '武汉', NOW(), NOW()),
+('10008', 'user008@example.com', '13800138008', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '吴十', '吴', '项目经理', '西安', NOW(), NOW()),
+('10009', 'user009@example.com', '13800138009', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '郑十一', '郑', '架构师', '南京', NOW(), NOW()),
+('10010', 'user010@example.com', '13800138010', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王十二', '王', '技术总监', '苏州', NOW(), NOW()),
+('10011', 'user011@example.com', '13800138011', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '冯十三', '冯', 'HR经理', '天津', NOW(), NOW()),
+('10012', 'user012@example.com', '13800138012', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '陈十四', '陈', '财务经理', '重庆', NOW(), NOW()),
+('10013', 'user013@example.com', '13800138013', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '褚十五', '褚', '市场经理', '长沙', NOW(), NOW()),
+('10014', 'user014@example.com', '13800138014', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '卫十六', '卫', '销售经理', '郑州', NOW(), NOW()),
+('10015', 'user015@example.com', '13800138015', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '蒋十七', '蒋', '客服主管', '济南', NOW(), NOW()),
+('10016', 'user016@example.com', '13800138016', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '沈十八', '沈', '运营专员', '青岛', NOW(), NOW()),
+('10017', 'user017@example.com', '13800138017', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '韩十九', '韩', '内容编辑', '大连', NOW(), NOW()),
+('10018', 'user018@example.com', '13800138018', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '杨二十', '杨', '商务拓展', '厦门', NOW(), NOW()),
+('10019', 'user019@example.com', '13800138019', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '朱二一', '朱', '法务专员', '福州', NOW(), NOW()),
+('10020', 'user020@example.com', '13800138020', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '秦二二', '秦', '行政助理', '合肥', NOW(), NOW());
+
+-- 说明:
+-- 1. 所有用户默认密码为:12345678
+-- 2. 密码已使用bcrypt加密存储
+-- 3. 用户ID从10001开始,连续到10020
+-- 4. 邮箱格式:user001@example.com 到 user020@example.com
+-- 5. 手机号格式:13800138001 到 13800138020
+-- 6. 头像使用用户姓氏的首字符
+-- 7. 每个用户都有不同的职位和地区信息
+