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

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