diff --git a/api/captcha_api/dto/request_dto.go b/api/captcha_api/dto/request_dto.go index 887c71e..9991310 100644 --- a/api/captcha_api/dto/request_dto.go +++ b/api/captcha_api/dto/request_dto.go @@ -1,6 +1,6 @@ package dto type RotateCaptchaRequest struct { - Angle int `json:"angle"` - Key string `json:"key"` + Angle int `json:"angle" binding:"required"` + Key string `json:"key" binding:"required"` } diff --git a/api/comment_api/comment.go b/api/comment_api/comment.go index b86bc19..118464e 100644 --- a/api/comment_api/comment.go +++ b/api/comment_api/comment.go @@ -1,14 +1,22 @@ package comment_api import ( + "encoding/base64" + "errors" + "io" + "regexp" "schisandra-cloud-album/service" + "strings" + "sync" "time" ) type CommentAPI struct{} +var wg sync.WaitGroup var commentReplyService = service.Service.CommentReplyService +// CommentImages 评论图片 type CommentImages struct { TopicId string `json:"topic_id" bson:"topic_id" required:"true"` CommentId int64 `json:"comment_id" bson:"comment_id" required:"true"` @@ -17,6 +25,7 @@ type CommentImages struct { CreatedAt string `json:"created_at" bson:"created_at" required:"true"` } +// CommentContent 评论内容 type CommentContent struct { NickName string `json:"nickname"` Avatar string `json:"avatar"` @@ -40,9 +49,88 @@ type CommentContent struct { Images []string `json:"images,omitempty"` } +// CommentResponse 评论返回值 type CommentResponse struct { Size int `json:"size"` Total int64 `json:"total"` Current int `json:"current"` Comments []CommentContent `json:"comments"` } + +// base64ToBytes 将base64字符串转换为字节数组 +func base64ToBytes(base64Str string) ([]byte, error) { + reader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64Str)) + data, err := io.ReadAll(reader) + if err != nil { + return nil, errors.New("failed to decode base64 string") + } + return data, nil +} + +// processImages 处理图片,将 base64 字符串转换为字节数组 +func processImages(images []string) ([][]byte, error) { + var imagesData [][]byte + dataChan := make(chan []byte, len(images)) // 创建一个带缓冲的 channel + re := regexp.MustCompile(`^data:image/\w+;base64,`) + + for _, img := range images { + wg.Add(1) // 增加 WaitGroup 的计数 + go func(img string) { + defer wg.Done() // 函数结束时减少计数 + + imgWithoutPrefix := re.ReplaceAllString(img, "") + data, err := base64ToBytes(imgWithoutPrefix) + if err != nil { + return // 出错时直接返回 + } + dataChan <- data // 将结果发送到 channel + }(img) + } + + wg.Wait() // 等待所有 goroutine 完成 + close(dataChan) // 关闭 channel + + for data := range dataChan { // 收集所有结果 + imagesData = append(imagesData, data) + } + + return imagesData, nil +} + +// getMimeType 获取 MIME 类型 +func getMimeType(data []byte) string { + if len(data) < 4 { + return "application/octet-stream" // 默认类型 + } + + // 判断 JPEG + if data[0] == 0xFF && data[1] == 0xD8 { + return "image/jpeg" + } + + // 判断 PNG + if len(data) >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 && + data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A { + return "image/png" + } + + // 判断 GIF + if len(data) >= 6 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F' { + return "image/gif" + } + // 判断 WEBP + if len(data) >= 12 && data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 && + data[8] == 0x57 && data[9] == 0x45 && data[10] == 0x42 && data[11] == 0x50 { + return "image/webp" + } + // 判断svg + if len(data) >= 4 && data[0] == '<' && data[1] == '?' && data[2] == 'x' && data[3] == 'm' { + return "image/svg+xml" + } + // 判断JPG + if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF { + return "image/jpeg" + } + + return "application/octet-stream" // 默认类型 +} diff --git a/api/comment_api/comment_api.go b/api/comment_api/comment_api.go index 9b07f5d..ba2fa6c 100644 --- a/api/comment_api/comment_api.go +++ b/api/comment_api/comment_api.go @@ -11,16 +11,12 @@ import ( "github.com/mssola/useragent" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" - "io" - "regexp" "schisandra-cloud-album/api/comment_api/dto" "schisandra-cloud-album/common/enum" "schisandra-cloud-album/common/result" "schisandra-cloud-album/global" "schisandra-cloud-album/model" "schisandra-cloud-album/utils" - "strconv" - "strings" "time" ) @@ -38,11 +34,6 @@ func (CommentAPI) CommentSubmit(c *gin.Context) { result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) return } - - if commentRequest.Content == "" || commentRequest.UserID == "" || commentRequest.TopicId == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } if len(commentRequest.Images) > 3 { result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c) return @@ -69,12 +60,14 @@ func (CommentAPI) CommentSubmit(c *gin.Context) { if commentRequest.UserID == commentRequest.Author { isAuthor = 1 } + tx := global.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() + commentReply := model.ScaCommentReply{ Content: commentRequest.Content, UserId: commentRequest.UserID, @@ -88,26 +81,38 @@ func (CommentAPI) CommentSubmit(c *gin.Context) { OperatingSystem: operatingSystem, Agent: userAgent, } + // 使用 goroutine 进行异步评论保存 + errCh := make(chan error, 2) + go func() { + errCh <- commentReplyService.CreateCommentReply(&commentReply) + }() - if err = commentReplyService.CreateCommentReply(&commentReply); err != nil { + // 等待评论回复的创建 + if err = <-errCh; err != nil { + global.LOG.Errorln(err) result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) tx.Rollback() return } + // 处理图片异步上传 if len(commentRequest.Images) > 0 { - - var imagesData [][]byte - for _, img := range commentRequest.Images { - re := regexp.MustCompile(`^data:image/\w+;base64,`) - imgWithoutPrefix := re.ReplaceAllString(img, "") - data, err := base64ToBytes(imgWithoutPrefix) + imagesDataCh := make(chan [][]byte) + go func() { + imagesData, err := processImages(commentRequest.Images) if err != nil { global.LOG.Errorln(err) - tx.Rollback() + imagesDataCh <- nil // 发送失败信号 return } - imagesData = append(imagesData, data) + imagesDataCh <- imagesData // 发送处理成功的数据 + }() + + imagesData := <-imagesDataCh + if imagesData == nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) + tx.Rollback() + return } commentImages := CommentImages{ @@ -117,28 +122,19 @@ func (CommentAPI) CommentSubmit(c *gin.Context) { Images: imagesData, CreatedAt: time.Now().Format("2006-01-02 15:04:05"), } - if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { - global.LOG.Errorln(err) - result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) - tx.Rollback() - return - } + + // 使用 goroutine 进行异步图片保存 + go func() { + if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { + global.LOG.Errorln(err) + } + }() } tx.Commit() result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c) return } -// base64ToBytes 将base64字符串转换为字节数组 -func base64ToBytes(base64Str string) ([]byte, error) { - reader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64Str)) - data, err := io.ReadAll(reader) - if err != nil { - return nil, errors.New("failed to decode base64 string") - } - return data, nil -} - // ReplySubmit 提交回复 // @Summary 提交回复 // @Description 提交回复 @@ -154,14 +150,6 @@ func (CommentAPI) ReplySubmit(c *gin.Context) { return } - if replyCommentRequest.Content == "" || - replyCommentRequest.UserID == "" || - replyCommentRequest.TopicId == "" || - strconv.FormatInt(replyCommentRequest.ReplyId, 10) == "" || - replyCommentRequest.ReplyUser == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } if len(replyCommentRequest.Images) > 3 { result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c) return @@ -172,8 +160,8 @@ func (CommentAPI) ReplySubmit(c *gin.Context) { global.LOG.Errorln("user-agent is empty") return } - ua := useragent.New(userAgent) + ua := useragent.New(userAgent) ip := utils.GetClientIP(c) location, err := global.IP2Location.SearchByStr(ip) if err != nil { @@ -188,12 +176,14 @@ func (CommentAPI) ReplySubmit(c *gin.Context) { if replyCommentRequest.UserID == replyCommentRequest.Author { isAuthor = 1 } + tx := global.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() + commentReply := model.ScaCommentReply{ Content: replyCommentRequest.Content, UserId: replyCommentRequest.UserID, @@ -209,33 +199,44 @@ func (CommentAPI) ReplySubmit(c *gin.Context) { OperatingSystem: operatingSystem, Agent: userAgent, } + // 使用 goroutine 进行异步评论保存 + errCh := make(chan error) + go func() { - if err = commentReplyService.CreateCommentReply(&commentReply); err != nil { - result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) - tx.Rollback() - return - } - err = commentReplyService.UpdateCommentReplyCount(replyCommentRequest.ReplyId) - if err != nil { + errCh <- commentReplyService.CreateCommentReply(&commentReply) + }() + go func() { + + errCh <- commentReplyService.UpdateCommentReplyCount(replyCommentRequest.ReplyId) + }() + // 等待评论回复的创建 + if err = <-errCh; err != nil { + global.LOG.Errorln(err) result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) tx.Rollback() return } + // 处理图片异步上传 if len(replyCommentRequest.Images) > 0 { - - var imagesData [][]byte - for _, img := range replyCommentRequest.Images { - re := regexp.MustCompile(`^data:image/\w+;base64,`) - imgWithoutPrefix := re.ReplaceAllString(img, "") - data, err := base64ToBytes(imgWithoutPrefix) + imagesDataCh := make(chan [][]byte) + go func() { + imagesData, err := processImages(replyCommentRequest.Images) if err != nil { global.LOG.Errorln(err) - tx.Rollback() + imagesDataCh <- nil // 发送失败信号 return } - imagesData = append(imagesData, data) + imagesDataCh <- imagesData // 发送处理成功的数据 + }() + + imagesData := <-imagesDataCh + if imagesData == nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) + tx.Rollback() + return } + commentImages := CommentImages{ TopicId: replyCommentRequest.TopicId, CommentId: commentReply.Id, @@ -243,12 +244,13 @@ func (CommentAPI) ReplySubmit(c *gin.Context) { Images: imagesData, CreatedAt: time.Now().Format("2006-01-02 15:04:05"), } - if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { - global.LOG.Errorln(err) - result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) - tx.Rollback() - return - } + + // 使用 goroutine 进行异步图片保存 + go func() { + if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { + global.LOG.Errorln(err) + } + }() } tx.Commit() result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c) @@ -270,15 +272,6 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) { return } - if replyReplyRequest.Content == "" || - replyReplyRequest.UserID == "" || - replyReplyRequest.TopicId == "" || - replyReplyRequest.ReplyTo == 0 || - replyReplyRequest.ReplyId == 0 || - replyReplyRequest.ReplyUser == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } if len(replyReplyRequest.Images) > 3 { result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c) return @@ -289,8 +282,8 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) { global.LOG.Errorln("user-agent is empty") return } - ua := useragent.New(userAgent) + ua := useragent.New(userAgent) ip := utils.GetClientIP(c) location, err := global.IP2Location.SearchByStr(ip) if err != nil { @@ -305,12 +298,14 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) { if replyReplyRequest.UserID == replyReplyRequest.Author { isAuthor = 1 } + tx := global.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() + commentReply := model.ScaCommentReply{ Content: replyReplyRequest.Content, UserId: replyReplyRequest.UserID, @@ -328,32 +323,40 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) { Agent: userAgent, } - if err = commentReplyService.CreateCommentReply(&commentReply); err != nil { - result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) - tx.Rollback() - return - } - err = commentReplyService.UpdateCommentReplyCount(replyReplyRequest.ReplyId) - if err != nil { + errCh := make(chan error, 2) + go func() { + errCh <- commentReplyService.CreateCommentReply(&commentReply) + }() + go func() { + errCh <- commentReplyService.UpdateCommentReplyCount(replyReplyRequest.ReplyId) + }() + + if err = <-errCh; err != nil { + global.LOG.Errorln(err) result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) tx.Rollback() return } if len(replyReplyRequest.Images) > 0 { - - var imagesData [][]byte - for _, img := range replyReplyRequest.Images { - re := regexp.MustCompile(`^data:image/\w+;base64,`) - imgWithoutPrefix := re.ReplaceAllString(img, "") - data, err := base64ToBytes(imgWithoutPrefix) + imagesDataCh := make(chan [][]byte) + go func() { + imagesData, err := processImages(replyReplyRequest.Images) if err != nil { global.LOG.Errorln(err) - tx.Rollback() + imagesDataCh <- nil return } - imagesData = append(imagesData, data) + imagesDataCh <- imagesData + }() + + imagesData := <-imagesDataCh + if imagesData == nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) + tx.Rollback() + return } + commentImages := CommentImages{ TopicId: replyReplyRequest.TopicId, CommentId: commentReply.Id, @@ -361,12 +364,13 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) { Images: imagesData, CreatedAt: time.Now().Format("2006-01-02 15:04:05"), } - if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { - global.LOG.Errorln(err) - result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) - tx.Rollback() - return - } + + // 处理图片保存 + go func() { + if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { + global.LOG.Errorln(err) + } + }() } tx.Commit() result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c) @@ -388,38 +392,57 @@ func (CommentAPI) CommentList(c *gin.Context) { result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) return } - if commentListRequest.TopicId == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } query, u := gplus.NewQuery[model.ScaCommentReply]() page := gplus.NewPage[model.ScaCommentReply](commentListRequest.Page, commentListRequest.Size) query.Eq(&u.TopicId, commentListRequest.TopicId).Eq(&u.CommentType, enum.COMMENT).OrderByDesc(&u.Likes) page, _ = gplus.SelectPage(page, query) - var commentsWithImages []CommentContent - + userIds := make([]string, 0, len(page.Records)) for _, comment := range page.Records { - // 获取评论图片 - commentImages := CommentImages{} - wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(context.Background(), bson.M{"comment_id": comment.Id}).Decode(&commentImages) - if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) { - global.LOG.Errorln(wrong) - } - // 将图片转换为base64 - var imagesBase64 []string - for _, img := range commentImages.Images { - mimeType := getMimeType(img) // 动态获取 MIME 类型 - base64Img := base64.StdEncoding.EncodeToString(img) - base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img) - imagesBase64 = append(imagesBase64, base64WithPrefix) - } - // 组装评论数据 - queryUser, n := gplus.NewQuery[model.ScaAuthUser]() - queryUser.Eq(&n.UID, comment.UserId) - userInfo, _ := gplus.SelectOne(queryUser) - commentsWithImages = append(commentsWithImages, - CommentContent{ + userIds = append(userIds, comment.UserId) + } + + queryUser, n := gplus.NewQuery[model.ScaAuthUser]() + queryUser.In(&n.UID, userIds) + userInfos, _ := gplus.SelectList(queryUser) + + userInfoMap := make(map[string]model.ScaAuthUser, len(userInfos)) + for _, userInfo := range userInfos { + userInfoMap[*userInfo.UID] = *userInfo + } + + commentChannel := make(chan CommentContent, len(page.Records)) + imagesBase64S := make([][]string, len(page.Records)) // 存储每条评论的图片 + + for index, comment := range page.Records { + wg.Add(1) + go func(comment model.ScaCommentReply, index int) { + defer wg.Done() + + // 使用 context 设置超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置超时,2秒 + defer cancel() + + // 获取评论图片并处理 + commentImages := CommentImages{} + wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(ctx, bson.M{"comment_id": comment.Id}).Decode(&commentImages) + if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) { + global.LOG.Errorf("Failed to get images for comment ID %s: %v", comment.Id, wrong) + return + } + + // 将图片转换为base64 + var imagesBase64 []string + for _, img := range commentImages.Images { + mimeType := getMimeType(img) + base64Img := base64.StdEncoding.EncodeToString(img) + base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img) + imagesBase64 = append(imagesBase64, base64WithPrefix) + } + imagesBase64S[index] = imagesBase64 // 保存到切片中 + + userInfo := userInfoMap[comment.UserId] + commentContent := CommentContent{ Avatar: *userInfo.Avatar, NickName: *userInfo.Nickname, Id: comment.Id, @@ -435,8 +458,21 @@ func (CommentAPI) CommentList(c *gin.Context) { Browser: comment.Browser, OperatingSystem: comment.OperatingSystem, Images: imagesBase64, - }) + } + commentChannel <- commentContent + }(*comment, index) } + + go func() { + wg.Wait() + close(commentChannel) + }() + + var commentsWithImages []CommentContent + for commentContent := range commentChannel { + commentsWithImages = append(commentsWithImages, commentContent) + } + response := CommentResponse{ Comments: commentsWithImages, Size: page.Size, @@ -447,30 +483,6 @@ func (CommentAPI) CommentList(c *gin.Context) { return } -func getMimeType(data []byte) string { - if len(data) < 4 { - return "application/octet-stream" // 默认类型 - } - - // 判断 JPEG - if data[0] == 0xFF && data[1] == 0xD8 { - return "image/jpeg" - } - - // 判断 PNG - if len(data) >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 && - data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A { - return "image/png" - } - - // 判断 GIF - if len(data) >= 6 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F' { - return "image/gif" - } - - return "application/octet-stream" // 默认类型 -} - // ReplyList 获取回复列表 // @Summary 获取回复列表 // @Description 获取回复列表 @@ -486,65 +498,113 @@ func (CommentAPI) ReplyList(c *gin.Context) { result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) return } - if replyListRequest.TopicId == "" || strconv.FormatInt(replyListRequest.CommentId, 10) == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } query, u := gplus.NewQuery[model.ScaCommentReply]() page := gplus.NewPage[model.ScaCommentReply](replyListRequest.Page, replyListRequest.Size) query.Eq(&u.TopicId, replyListRequest.TopicId).Eq(&u.ReplyId, replyListRequest.CommentId).Eq(&u.CommentType, enum.REPLY).OrderByDesc(&u.Likes) page, _ = gplus.SelectPage(page, query) - var commentsWithImages []CommentContent + userIds := make([]string, 0, len(page.Records)) + replyUserIds := make([]string, 0, len(page.Records)) + // 收集用户 ID 和回复用户 ID for _, comment := range page.Records { - // 获取评论图片 - commentImages := CommentImages{} - wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(context.Background(), bson.M{"comment_id": comment.Id}).Decode(&commentImages) - if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) { - global.LOG.Errorln(wrong) + userIds = append(userIds, comment.UserId) + if comment.ReplyUser != "" { + replyUserIds = append(replyUserIds, comment.ReplyUser) } - // 将图片转换为base64 - var imagesBase64 []string - for _, img := range commentImages.Images { - mimeType := getMimeType(img) // 动态获取 MIME 类型 - base64Img := base64.StdEncoding.EncodeToString(img) - base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img) - imagesBase64 = append(imagesBase64, base64WithPrefix) - } - // 查询评论用户信息 - queryUser, n := gplus.NewQuery[model.ScaAuthUser]() - queryUser.Eq(&n.UID, comment.UserId) - userInfo, _ := gplus.SelectOne(queryUser) - // 查询回复用户信息 + } + + // 查询评论用户信息 + queryUser, n := gplus.NewQuery[model.ScaAuthUser]() + queryUser.In(&n.UID, userIds) + userInfos, _ := gplus.SelectList(queryUser) + + userInfoMap := make(map[string]model.ScaAuthUser, len(userInfos)) + for _, userInfo := range userInfos { + userInfoMap[*userInfo.UID] = *userInfo + } + + // 查询回复用户信息 + replyUserInfoMap := make(map[string]model.ScaAuthUser) + if len(replyUserIds) > 0 { queryReplyUser, m := gplus.NewQuery[model.ScaAuthUser]() - queryReplyUser.Eq(&m.UID, comment.ReplyUser) - replyUserInfo, _ := gplus.SelectOne(queryReplyUser) - commentsWithImages = append(commentsWithImages, - CommentContent{ + queryReplyUser.In(&m.UID, replyUserIds) + replyUserInfos, _ := gplus.SelectList(queryReplyUser) + + for _, replyUserInfo := range replyUserInfos { + replyUserInfoMap[*replyUserInfo.UID] = *replyUserInfo + } + } + + replyChannel := make(chan CommentContent, len(page.Records)) // 使用通道传递回复内容 + imagesBase64S := make([][]string, len(page.Records)) // 存储每条回复的图片 + + for index, reply := range page.Records { + wg.Add(1) + go func(reply model.ScaCommentReply, index int) { + defer wg.Done() + + // 使用 context 设置超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // 设置超时,2秒 + defer cancel() + + // 获取回复图片并处理 + replyImages := CommentImages{} + wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(ctx, bson.M{"comment_id": reply.Id}).Decode(&replyImages) + if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) { + global.LOG.Errorf("Failed to get images for reply ID %s: %v", reply.Id, wrong) + return + } + + // 将图片转换为base64 + var imagesBase64 []string + for _, img := range replyImages.Images { + mimeType := getMimeType(img) + base64Img := base64.StdEncoding.EncodeToString(img) + base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img) + imagesBase64 = append(imagesBase64, base64WithPrefix) + } + imagesBase64S[index] = imagesBase64 // 保存到切片中 + + userInfo := userInfoMap[reply.UserId] + replyUserInfo := replyUserInfoMap[reply.ReplyUser] + commentContent := CommentContent{ Avatar: *userInfo.Avatar, NickName: *userInfo.Nickname, - Id: comment.Id, - UserId: comment.UserId, - TopicId: comment.TopicId, - Dislikes: comment.Dislikes, - Content: comment.Content, + Id: reply.Id, + UserId: reply.UserId, + TopicId: reply.TopicId, + Dislikes: reply.Dislikes, + Content: reply.Content, ReplyUsername: *replyUserInfo.Nickname, - ReplyCount: comment.ReplyCount, - Likes: comment.Likes, - CreatedTime: comment.CreatedTime, - ReplyUser: comment.ReplyUser, - ReplyId: comment.ReplyId, - ReplyTo: comment.ReplyTo, - Author: comment.Author, - Location: comment.Location, - Browser: comment.Browser, - OperatingSystem: comment.OperatingSystem, + ReplyCount: reply.ReplyCount, + Likes: reply.Likes, + CreatedTime: reply.CreatedTime, + ReplyUser: reply.ReplyUser, + ReplyId: reply.ReplyId, + ReplyTo: reply.ReplyTo, + Author: reply.Author, + Location: reply.Location, + Browser: reply.Browser, + OperatingSystem: reply.OperatingSystem, Images: imagesBase64, - }) + } + replyChannel <- commentContent // 发送到通道 + }(*reply, index) } + + go func() { + wg.Wait() + close(replyChannel) // 关闭通道 + }() + + var repliesWithImages []CommentContent + for replyContent := range replyChannel { + repliesWithImages = append(repliesWithImages, replyContent) // 从通道获取回复内容 + } + response := CommentResponse{ - Comments: commentsWithImages, + Comments: repliesWithImages, Size: page.Size, Current: page.Current, Total: page.Total, @@ -552,3 +612,17 @@ func (CommentAPI) ReplyList(c *gin.Context) { result.OkWithData(response, c) return } + +// CommentLikes 点赞评论 +func (CommentAPI) CommentLikes(c *gin.Context) { + likeRequest := dto.CommentLikeRequest{} + err := c.ShouldBindJSON(&likeRequest) + if err != nil { + result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) + return + } +} + +func (CommentAPI) CommentDislikes(c *gin.Context) { + +} diff --git a/api/comment_api/dto/request_dto.go b/api/comment_api/dto/request_dto.go index 5edd603..a0e536e 100644 --- a/api/comment_api/dto/request_dto.go +++ b/api/comment_api/dto/request_dto.go @@ -1,41 +1,47 @@ package dto type CommentRequest struct { - Content string `json:"content"` + Content string `json:"content" binding:"required"` Images []string `json:"images"` - UserID string `json:"user_id"` - TopicId string `json:"topic_id"` - Author string `json:"author"` + UserID string `json:"user_id" binding:"required"` + TopicId string `json:"topic_id" binding:"required"` + Author string `json:"author" binding:"required"` } type ReplyCommentRequest struct { - Content string `json:"content"` + Content string `json:"content" binding:"required"` Images []string `json:"images"` - UserID string `json:"user_id"` - TopicId string `json:"topic_id"` - ReplyId int64 `json:"reply_id"` - ReplyUser string `json:"reply_user"` - Author string `json:"author"` + UserID string `json:"user_id" binding:"required"` + TopicId string `json:"topic_id" binding:"required"` + ReplyId int64 `json:"reply_id" binding:"required"` + ReplyUser string `json:"reply_user" binding:"required"` + Author string `json:"author" binding:"required"` } type ReplyReplyRequest struct { - Content string `json:"content"` + Content string `json:"content" binding:"required"` Images []string `json:"images"` - UserID string `json:"user_id"` - TopicId string `json:"topic_id"` - ReplyTo int64 `json:"reply_to"` - ReplyId int64 `json:"reply_id"` - ReplyUser string `json:"reply_user"` - Author string `json:"author"` + UserID string `json:"user_id" binding:"required"` + TopicId string `json:"topic_id" binding:"required"` + ReplyTo int64 `json:"reply_to" binding:"required"` + ReplyId int64 `json:"reply_id" binding:"required"` + ReplyUser string `json:"reply_user" binding:"required"` + Author string `json:"author" binding:"required""` } type CommentListRequest struct { - TopicId string `json:"topic_id"` + TopicId string `json:"topic_id" binding:"required"` Page int `json:"page" default:"1"` Size int `json:"size" default:"5"` } type ReplyListRequest struct { - TopicId string `json:"topic_id"` - CommentId int64 `json:"comment_id"` + TopicId string `json:"topic_id" binding:"required"` + CommentId int64 `json:"comment_id" binding:"required"` Page int `json:"page" default:"1"` Size int `json:"size" default:"5"` } +type CommentLikeRequest struct { + TopicId string `json:"topic_id" binding:"required"` + CommentId int64 `json:"comment_id" binding:"required"` + UserID string `json:"user_id" binding:"required"` + Type int `json:"type" binding:"required"` +} diff --git a/api/oauth_api/wechat_api.go b/api/oauth_api/wechat_api.go index b0c9f49..39c0df4 100644 --- a/api/oauth_api/wechat_api.go +++ b/api/oauth_api/wechat_api.go @@ -301,11 +301,11 @@ func handelUserLogin(userId string, clientId string) bool { resultChan <- false return } - responseData := map[string]interface{}{ - "code": 0, - "message": "success", - "data": data, - "success": true, + responseData := result.Response{ + Data: data, + Message: "success", + Code: 200, + Success: true, } tokenData, err := json.Marshal(responseData) if err != nil { diff --git a/api/role_api/dto/request_dto.go b/api/role_api/dto/request_dto.go index 13be17c..f224806 100644 --- a/api/role_api/dto/request_dto.go +++ b/api/role_api/dto/request_dto.go @@ -1,11 +1,11 @@ package dto type RoleRequestDto struct { - RoleName string `json:"role_name"` - RoleKey string `json:"role_key"` + RoleName string `json:"role_name" binding:"required"` + RoleKey string `json:"role_key" binding:"required"` } type AddRoleToUserRequestDto struct { - Uid string `json:"uid"` - RoleKey string `json:"role_key"` + Uid string `json:"uid" binding:"required"` + RoleKey string `json:"role_key" binding:"required"` } diff --git a/api/user_api/dto/request_dto.go b/api/user_api/dto/request_dto.go index 4bd430f..c22ed14 100644 --- a/api/user_api/dto/request_dto.go +++ b/api/user_api/dto/request_dto.go @@ -4,36 +4,36 @@ import "encoding/json" // RefreshTokenRequest 刷新token请求 type RefreshTokenRequest struct { - RefreshToken string `json:"refresh_token"` + RefreshToken string `json:"refresh_token" binding:"required"` } // PhoneLoginRequest 手机号登录请求 type PhoneLoginRequest struct { - Phone string `json:"phone"` - Captcha string `json:"captcha"` - AutoLogin bool `json:"auto_login"` + Phone string `json:"phone" binding:"required"` + Captcha string `json:"captcha" binding:"required"` + AutoLogin bool `json:"auto_login" binding:"required"` } // AccountLoginRequest 账号登录请求 type AccountLoginRequest struct { - Account string `json:"account"` - Password string `json:"password"` - AutoLogin bool `json:"auto_login"` + Account string `json:"account" binding:"required"` + Password string `json:"password" binding:"required"` + AutoLogin bool `json:"auto_login" binding:"required"` } // AddUserRequest 新增用户请求 type AddUserRequest struct { - Username string `json:"username"` - Password string `json:"password"` - Phone string `json:"phone"` + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + Phone string `json:"phone" binding:"required"` } // ResetPasswordRequest 重置密码请求 type ResetPasswordRequest struct { - Phone string `json:"phone"` - Captcha string `json:"captcha"` - Password string `json:"password"` - Repassword string `json:"repassword"` + Phone string `json:"phone" binding:"required"` + Captcha string `json:"captcha" binding:"required"` + Password string `json:"password" binding:"required"` + Repassword string `json:"repassword" binding:"required"` } // ResponseData 返回数据 diff --git a/api/user_api/user_api.go b/api/user_api/user_api.go index 29da012..21e2cd9 100644 --- a/api/user_api/user_api.go +++ b/api/user_api/user_api.go @@ -118,10 +118,6 @@ func (UserAPI) AccountLogin(c *gin.Context) { } account := accountLoginRequest.Account password := accountLoginRequest.Password - if account == "" || password == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "AccountAndPasswordNotEmpty"), c) - return - } var user model.ScaAuthUser if utils.IsPhone(account) { @@ -163,10 +159,6 @@ func (UserAPI) PhoneLogin(c *gin.Context) { phone := request.Phone captcha := request.Captcha autoLogin := request.AutoLogin - if phone == "" || captcha == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneAndCaptchaNotEmpty"), c) - return - } if !utils.IsPhone(phone) { result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c) return @@ -273,10 +265,6 @@ func (UserAPI) RefreshHandler(c *gin.Context) { return } refreshToken := request.RefreshToken - if refreshToken == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } parseRefreshToken, isUpd, err := utils.ParseRefreshToken(refreshToken) if err != nil || !isUpd { global.LOG.Errorln(err) @@ -369,12 +357,6 @@ func (UserAPI) ResetPassword(c *gin.Context) { captcha := resetPasswordRequest.Captcha password := resetPasswordRequest.Password repassword := resetPasswordRequest.Repassword - - if phone == "" || captcha == "" || password == "" || repassword == "" { - result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) - return - } - if !utils.IsPhone(phone) { result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c) return diff --git a/config/conf_mongodb.go b/config/conf_mongodb.go index 800907b..cace0a2 100644 --- a/config/conf_mongodb.go +++ b/config/conf_mongodb.go @@ -9,8 +9,9 @@ type MongoDB struct { DB string `yaml:"db"` User string `yaml:"user"` Password string `yaml:"password"` - MaxOpenConn int `yaml:"max-open-conn"` - MaxIdleConn int `yaml:"max-idle-conn"` + MaxOpenConn uint64 `yaml:"max-open-conn"` + MaxIdleConn uint64 `yaml:"max-idle-conn"` + Timeout int `yaml:"timeout"` } func (m *MongoDB) MongoDsn() string { diff --git a/core/mongodb.go b/core/mongodb.go index 25b164f..fb54d6b 100644 --- a/core/mongodb.go +++ b/core/mongodb.go @@ -2,7 +2,6 @@ package core import ( "context" - "fmt" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "schisandra-cloud-album/global" @@ -24,6 +23,9 @@ func InitMongoDB() { Password: global.CONFIG.MongoDB.Password, PasswordSet: true, }) + clientOptions.SetConnectTimeout(time.Duration(global.CONFIG.MongoDB.Timeout) * time.Second) + clientOptions.SetMaxPoolSize(global.CONFIG.MongoDB.MaxOpenConn) + clientOptions.SetMaxConnecting(global.CONFIG.MongoDB.MaxIdleConn) connect, err := mongo.Connect(ctx, clientOptions) if err != nil { global.LOG.Fatalf(err.Error()) @@ -36,5 +38,4 @@ func InitMongoDB() { return } global.MongoDB = connect - fmt.Println("Connected to MongoDB!") } diff --git a/model/sca_comment_likes.go b/model/sca_comment_likes.go index 60bf8e6..f060ca7 100644 --- a/model/sca_comment_likes.go +++ b/model/sca_comment_likes.go @@ -5,6 +5,7 @@ const ScaCommentLikesTableName = "sca_comment_likes" // ScaCommentLikes 评论点赞表 type ScaCommentLikes struct { Id int64 `gorm:"column:id;type:bigint(20);primary_key" json:"id"` + TopicId string `gorm:"column:topic_id;type:varchar(20);NOT NULL;comment:主题ID" json:"topic_id"` UserId string `gorm:"column:user_id;type:varchar(20);comment:用户ID;NOT NULL" json:"user_id"` CommentId int64 `gorm:"column:comment_id;type:bigint(20);comment:评论ID;NOT NULL" json:"comment_id"` Type int `gorm:"column:type;type:int(11);comment:类型,0 点赞 1 踩;NOT NULL" json:"type"` diff --git a/service/comment_reply_service/comment_reply_service.go b/service/comment_reply_service/comment_reply_service.go index 4cb623c..3bc42aa 100644 --- a/service/comment_reply_service/comment_reply_service.go +++ b/service/comment_reply_service/comment_reply_service.go @@ -45,7 +45,7 @@ func (CommentReplyService) GetCommentListOrderByLikesDesc(topicID uint, page, pa func (CommentReplyService) UpdateCommentReplyCount(commentID int64) error { // 使用事务处理错误 err := global.DB.Transaction(func(tx *gorm.DB) error { - result := tx.Model(&model.ScaCommentReply{}).Where("id = ?", commentID).Update("reply_count", gorm.Expr("reply_count + ?", 1)) + result := tx.Model(&model.ScaCommentReply{}).Where("id = ? and deleted = 0", commentID).Update("reply_count", gorm.Expr("reply_count + ?", 1)) if result.Error != nil { return result.Error // 返回更新错误 }