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

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -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"
}
]
}
}
}
]
}
]
}

View File

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

View File

@@ -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
# 认证领域名称

3
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -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,
}, "获取成功")
}

View File

@@ -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, // 仅开发环境,生产环境应移除
}, "验证码已发送")
}

View File

@@ -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, "已删除好友")
}

View File

@@ -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}, "获取成功")
}

View File

@@ -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, "获取成功")
}

View File

@@ -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)
}

112
internal/middleware/auth.go Normal file
View File

@@ -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 <token>
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()
}
}

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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"` // 接口信息
}

View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
}
}

166
internal/utils/jwt.go Normal file
View File

@@ -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
}

View File

@@ -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
}

172
internal/utils/response.go Normal file
View File

@@ -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)
}

182
internal/utils/snowflake.go Normal file
View File

@@ -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 数据中心ID0-31
* @param machineID 机器ID0-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)
}

BIN
server.exe Normal file

Binary file not shown.

View File

@@ -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. 每个用户都有不同的职位和地区信息