v2版本
This commit is contained in:
153
internal/api/attachment_handler.go
Normal file
153
internal/api/attachment_handler.go
Normal 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,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
225
internal/api/auth_handler.go
Normal file
225
internal/api/auth_handler.go
Normal 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, // 仅开发环境,生产环境应移除
|
||||
}, "验证码已发送")
|
||||
}
|
||||
|
||||
362
internal/api/contact_handler.go
Normal file
362
internal/api/contact_handler.go
Normal 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, "已删除好友")
|
||||
}
|
||||
|
||||
@@ -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}, "获取成功")
|
||||
}
|
||||
|
||||
71
internal/api/room_handler.go
Normal file
71
internal/api/room_handler.go
Normal 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, "获取成功")
|
||||
}
|
||||
|
||||
155
internal/api/user_handler.go
Normal file
155
internal/api/user_handler.go
Normal 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
112
internal/middleware/auth.go
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
173
internal/middleware/request_log.go
Normal file
173
internal/middleware/request_log.go
Normal 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)
|
||||
}
|
||||
|
||||
26
internal/middleware/response.go
Normal file
26
internal/middleware/response.go
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
28
internal/model/response.go
Normal file
28
internal/model/response.go
Normal 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"` // 接口信息
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
207
internal/service/attachment_service.go
Normal file
207
internal/service/attachment_service.go
Normal 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
|
||||
}
|
||||
|
||||
235
internal/service/auth_service.go
Normal file
235
internal/service/auth_service.go
Normal 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"
|
||||
}
|
||||
|
||||
310
internal/service/contact_service.go
Normal file
310
internal/service/contact_service.go
Normal 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
|
||||
}
|
||||
|
||||
56
internal/service/login_log_service.go
Normal file
56
internal/service/login_log_service.go
Normal 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)
|
||||
}
|
||||
|
||||
148
internal/service/room_service.go
Normal file
148
internal/service/room_service.go
Normal 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
|
||||
}
|
||||
|
||||
165
internal/service/user_service.go
Normal file
165
internal/service/user_service.go
Normal 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
|
||||
}
|
||||
|
||||
163
internal/utils/ip_location.go
Normal file
163
internal/utils/ip_location.go
Normal 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
166
internal/utils/jwt.go
Normal 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
|
||||
}
|
||||
|
||||
82
internal/utils/password.go
Normal file
82
internal/utils/password.go
Normal 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
172
internal/utils/response.go
Normal 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
182
internal/utils/snowflake.go
Normal 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 数据中心ID(0-31)
|
||||
* @param machineID 机器ID(0-31)
|
||||
*/
|
||||
func InitSnowflake(datacenterID, machineID int64) error {
|
||||
if datacenterID < 0 || datacenterID > maxDatacenterID {
|
||||
return errors.New("datacenter ID must be between 0 and 31")
|
||||
}
|
||||
if machineID < 0 || machineID > maxMachineID {
|
||||
return errors.New("machine ID must be between 0 and 31")
|
||||
}
|
||||
|
||||
once.Do(func() {
|
||||
globalSnowflake = &Snowflake{
|
||||
datacenterID: datacenterID,
|
||||
machineID: machineID,
|
||||
sequence: 0,
|
||||
lastStamp: -1,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* NextID
|
||||
* 功能:生成下一个ID
|
||||
* @returns 64位整数ID
|
||||
*/
|
||||
func NextID() (int64, error) {
|
||||
if globalSnowflake == nil {
|
||||
// 默认使用datacenterID=1, machineID=1
|
||||
if err := InitSnowflake(1, 1); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return globalSnowflake.nextID()
|
||||
}
|
||||
|
||||
/**
|
||||
* nextID
|
||||
* 功能:生成下一个ID(内部方法)
|
||||
*/
|
||||
func (s *Snowflake) nextID() (int64, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
// 如果当前时间小于上次时间,说明时钟回拨
|
||||
if now < s.lastStamp {
|
||||
return 0, errors.New("clock moved backwards")
|
||||
}
|
||||
|
||||
// 如果是同一毫秒内
|
||||
if now == s.lastStamp {
|
||||
s.sequence = (s.sequence + 1) & maxSequence
|
||||
// 序列号溢出,等待下一毫秒
|
||||
if s.sequence == 0 {
|
||||
now = s.waitNextMillis(s.lastStamp)
|
||||
}
|
||||
} else {
|
||||
// 新的毫秒,序列号重置
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
s.lastStamp = now
|
||||
|
||||
// 生成ID
|
||||
id := ((now - epoch) << timestampShift) |
|
||||
(s.datacenterID << datacenterIDShift) |
|
||||
(s.machineID << machineIDShift) |
|
||||
s.sequence
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* waitNextMillis
|
||||
* 功能:等待下一毫秒
|
||||
*/
|
||||
func (s *Snowflake) waitNextMillis(lastStamp int64) int64 {
|
||||
now := time.Now().UnixMilli()
|
||||
for now <= lastStamp {
|
||||
now = time.Now().UnixMilli()
|
||||
}
|
||||
return now
|
||||
}
|
||||
|
||||
/**
|
||||
* NextIDString
|
||||
* 功能:生成下一个ID(字符串格式)
|
||||
*/
|
||||
func NextIDString() (string, error) {
|
||||
id, err := NextID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return int64ToString(id), nil
|
||||
}
|
||||
|
||||
/**
|
||||
* int64ToString
|
||||
* 功能:将int64转换为字符串
|
||||
*/
|
||||
func int64ToString(id int64) string {
|
||||
if id == 0 {
|
||||
return "0"
|
||||
}
|
||||
negative := id < 0
|
||||
if negative {
|
||||
id = -id
|
||||
}
|
||||
|
||||
var result []byte
|
||||
for id > 0 {
|
||||
result = append([]byte{byte('0' + id%10)}, result...)
|
||||
id /= 10
|
||||
}
|
||||
|
||||
if negative {
|
||||
result = append([]byte{'-'}, result...)
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user