optimize the list of comments

This commit is contained in:
landaiqing
2024-09-24 21:37:37 +08:00
parent 5d86914cb7
commit bb2f49fe74
12 changed files with 411 additions and 258 deletions

View File

@@ -1,6 +1,6 @@
package dto package dto
type RotateCaptchaRequest struct { type RotateCaptchaRequest struct {
Angle int `json:"angle"` Angle int `json:"angle" binding:"required"`
Key string `json:"key"` Key string `json:"key" binding:"required"`
} }

View File

@@ -1,14 +1,22 @@
package comment_api package comment_api
import ( import (
"encoding/base64"
"errors"
"io"
"regexp"
"schisandra-cloud-album/service" "schisandra-cloud-album/service"
"strings"
"sync"
"time" "time"
) )
type CommentAPI struct{} type CommentAPI struct{}
var wg sync.WaitGroup
var commentReplyService = service.Service.CommentReplyService var commentReplyService = service.Service.CommentReplyService
// CommentImages 评论图片
type CommentImages struct { type CommentImages struct {
TopicId string `json:"topic_id" bson:"topic_id" required:"true"` TopicId string `json:"topic_id" bson:"topic_id" required:"true"`
CommentId int64 `json:"comment_id" bson:"comment_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"` CreatedAt string `json:"created_at" bson:"created_at" required:"true"`
} }
// CommentContent 评论内容
type CommentContent struct { type CommentContent struct {
NickName string `json:"nickname"` NickName string `json:"nickname"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
@@ -40,9 +49,88 @@ type CommentContent struct {
Images []string `json:"images,omitempty"` Images []string `json:"images,omitempty"`
} }
// CommentResponse 评论返回值
type CommentResponse struct { type CommentResponse struct {
Size int `json:"size"` Size int `json:"size"`
Total int64 `json:"total"` Total int64 `json:"total"`
Current int `json:"current"` Current int `json:"current"`
Comments []CommentContent `json:"comments"` 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" // 默认类型
}

View File

@@ -11,16 +11,12 @@ import (
"github.com/mssola/useragent" "github.com/mssola/useragent"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"io"
"regexp"
"schisandra-cloud-album/api/comment_api/dto" "schisandra-cloud-album/api/comment_api/dto"
"schisandra-cloud-album/common/enum" "schisandra-cloud-album/common/enum"
"schisandra-cloud-album/common/result" "schisandra-cloud-album/common/result"
"schisandra-cloud-album/global" "schisandra-cloud-album/global"
"schisandra-cloud-album/model" "schisandra-cloud-album/model"
"schisandra-cloud-album/utils" "schisandra-cloud-album/utils"
"strconv"
"strings"
"time" "time"
) )
@@ -38,11 +34,6 @@ func (CommentAPI) CommentSubmit(c *gin.Context) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return return
} }
if commentRequest.Content == "" || commentRequest.UserID == "" || commentRequest.TopicId == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
if len(commentRequest.Images) > 3 { if len(commentRequest.Images) > 3 {
result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c)
return return
@@ -69,12 +60,14 @@ func (CommentAPI) CommentSubmit(c *gin.Context) {
if commentRequest.UserID == commentRequest.Author { if commentRequest.UserID == commentRequest.Author {
isAuthor = 1 isAuthor = 1
} }
tx := global.DB.Begin() tx := global.DB.Begin()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
tx.Rollback() tx.Rollback()
} }
}() }()
commentReply := model.ScaCommentReply{ commentReply := model.ScaCommentReply{
Content: commentRequest.Content, Content: commentRequest.Content,
UserId: commentRequest.UserID, UserId: commentRequest.UserID,
@@ -88,26 +81,38 @@ func (CommentAPI) CommentSubmit(c *gin.Context) {
OperatingSystem: operatingSystem, OperatingSystem: operatingSystem,
Agent: userAgent, 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) result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback() tx.Rollback()
return return
} }
// 处理图片异步上传
if len(commentRequest.Images) > 0 { if len(commentRequest.Images) > 0 {
imagesDataCh := make(chan [][]byte)
var imagesData [][]byte go func() {
for _, img := range commentRequest.Images { imagesData, err := processImages(commentRequest.Images)
re := regexp.MustCompile(`^data:image/\w+;base64,`)
imgWithoutPrefix := re.ReplaceAllString(img, "")
data, err := base64ToBytes(imgWithoutPrefix)
if err != nil { if err != nil {
global.LOG.Errorln(err) global.LOG.Errorln(err)
tx.Rollback() imagesDataCh <- nil // 发送失败信号
return return
} }
imagesData = append(imagesData, data) imagesDataCh <- imagesData // 发送处理成功的数据
}()
imagesData := <-imagesDataCh
if imagesData == nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback()
return
} }
commentImages := CommentImages{ commentImages := CommentImages{
@@ -117,28 +122,19 @@ func (CommentAPI) CommentSubmit(c *gin.Context) {
Images: imagesData, Images: imagesData,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"), CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
} }
// 使用 goroutine 进行异步图片保存
go func() {
if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil {
global.LOG.Errorln(err) global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback()
return
} }
}()
} }
tx.Commit() tx.Commit()
result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c) result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c)
return 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 提交回复 // ReplySubmit 提交回复
// @Summary 提交回复 // @Summary 提交回复
// @Description 提交回复 // @Description 提交回复
@@ -154,14 +150,6 @@ func (CommentAPI) ReplySubmit(c *gin.Context) {
return 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 { if len(replyCommentRequest.Images) > 3 {
result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c)
return return
@@ -172,8 +160,8 @@ func (CommentAPI) ReplySubmit(c *gin.Context) {
global.LOG.Errorln("user-agent is empty") global.LOG.Errorln("user-agent is empty")
return return
} }
ua := useragent.New(userAgent)
ua := useragent.New(userAgent)
ip := utils.GetClientIP(c) ip := utils.GetClientIP(c)
location, err := global.IP2Location.SearchByStr(ip) location, err := global.IP2Location.SearchByStr(ip)
if err != nil { if err != nil {
@@ -188,12 +176,14 @@ func (CommentAPI) ReplySubmit(c *gin.Context) {
if replyCommentRequest.UserID == replyCommentRequest.Author { if replyCommentRequest.UserID == replyCommentRequest.Author {
isAuthor = 1 isAuthor = 1
} }
tx := global.DB.Begin() tx := global.DB.Begin()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
tx.Rollback() tx.Rollback()
} }
}() }()
commentReply := model.ScaCommentReply{ commentReply := model.ScaCommentReply{
Content: replyCommentRequest.Content, Content: replyCommentRequest.Content,
UserId: replyCommentRequest.UserID, UserId: replyCommentRequest.UserID,
@@ -209,33 +199,44 @@ func (CommentAPI) ReplySubmit(c *gin.Context) {
OperatingSystem: operatingSystem, OperatingSystem: operatingSystem,
Agent: userAgent, Agent: userAgent,
} }
// 使用 goroutine 进行异步评论保存
errCh := make(chan error)
go func() {
if err = commentReplyService.CreateCommentReply(&commentReply); err != nil { errCh <- commentReplyService.CreateCommentReply(&commentReply)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) }()
tx.Rollback() go func() {
return
} errCh <- commentReplyService.UpdateCommentReplyCount(replyCommentRequest.ReplyId)
err = commentReplyService.UpdateCommentReplyCount(replyCommentRequest.ReplyId) }()
if err != nil { // 等待评论回复的创建
if err = <-errCh; err != nil {
global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback() tx.Rollback()
return return
} }
// 处理图片异步上传
if len(replyCommentRequest.Images) > 0 { if len(replyCommentRequest.Images) > 0 {
imagesDataCh := make(chan [][]byte)
var imagesData [][]byte go func() {
for _, img := range replyCommentRequest.Images { imagesData, err := processImages(replyCommentRequest.Images)
re := regexp.MustCompile(`^data:image/\w+;base64,`)
imgWithoutPrefix := re.ReplaceAllString(img, "")
data, err := base64ToBytes(imgWithoutPrefix)
if err != nil { if err != nil {
global.LOG.Errorln(err) global.LOG.Errorln(err)
imagesDataCh <- nil // 发送失败信号
return
}
imagesDataCh <- imagesData // 发送处理成功的数据
}()
imagesData := <-imagesDataCh
if imagesData == nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback() tx.Rollback()
return return
} }
imagesData = append(imagesData, data)
}
commentImages := CommentImages{ commentImages := CommentImages{
TopicId: replyCommentRequest.TopicId, TopicId: replyCommentRequest.TopicId,
CommentId: commentReply.Id, CommentId: commentReply.Id,
@@ -243,12 +244,13 @@ func (CommentAPI) ReplySubmit(c *gin.Context) {
Images: imagesData, Images: imagesData,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"), CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
} }
// 使用 goroutine 进行异步图片保存
go func() {
if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil {
global.LOG.Errorln(err) global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback()
return
} }
}()
} }
tx.Commit() tx.Commit()
result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c) result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c)
@@ -270,15 +272,6 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) {
return 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 { if len(replyReplyRequest.Images) > 3 {
result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "TooManyImages"), c)
return return
@@ -289,8 +282,8 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) {
global.LOG.Errorln("user-agent is empty") global.LOG.Errorln("user-agent is empty")
return return
} }
ua := useragent.New(userAgent)
ua := useragent.New(userAgent)
ip := utils.GetClientIP(c) ip := utils.GetClientIP(c)
location, err := global.IP2Location.SearchByStr(ip) location, err := global.IP2Location.SearchByStr(ip)
if err != nil { if err != nil {
@@ -305,12 +298,14 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) {
if replyReplyRequest.UserID == replyReplyRequest.Author { if replyReplyRequest.UserID == replyReplyRequest.Author {
isAuthor = 1 isAuthor = 1
} }
tx := global.DB.Begin() tx := global.DB.Begin()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
tx.Rollback() tx.Rollback()
} }
}() }()
commentReply := model.ScaCommentReply{ commentReply := model.ScaCommentReply{
Content: replyReplyRequest.Content, Content: replyReplyRequest.Content,
UserId: replyReplyRequest.UserID, UserId: replyReplyRequest.UserID,
@@ -328,32 +323,40 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) {
Agent: userAgent, Agent: userAgent,
} }
if err = commentReplyService.CreateCommentReply(&commentReply); err != nil { errCh := make(chan error, 2)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) go func() {
tx.Rollback() errCh <- commentReplyService.CreateCommentReply(&commentReply)
return }()
} go func() {
err = commentReplyService.UpdateCommentReplyCount(replyReplyRequest.ReplyId) errCh <- commentReplyService.UpdateCommentReplyCount(replyReplyRequest.ReplyId)
if err != nil { }()
if err = <-errCh; err != nil {
global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback() tx.Rollback()
return return
} }
if len(replyReplyRequest.Images) > 0 { if len(replyReplyRequest.Images) > 0 {
imagesDataCh := make(chan [][]byte)
var imagesData [][]byte go func() {
for _, img := range replyReplyRequest.Images { imagesData, err := processImages(replyReplyRequest.Images)
re := regexp.MustCompile(`^data:image/\w+;base64,`)
imgWithoutPrefix := re.ReplaceAllString(img, "")
data, err := base64ToBytes(imgWithoutPrefix)
if err != nil { if err != nil {
global.LOG.Errorln(err) global.LOG.Errorln(err)
imagesDataCh <- nil
return
}
imagesDataCh <- imagesData
}()
imagesData := <-imagesDataCh
if imagesData == nil {
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback() tx.Rollback()
return return
} }
imagesData = append(imagesData, data)
}
commentImages := CommentImages{ commentImages := CommentImages{
TopicId: replyReplyRequest.TopicId, TopicId: replyReplyRequest.TopicId,
CommentId: commentReply.Id, CommentId: commentReply.Id,
@@ -361,12 +364,13 @@ func (CommentAPI) ReplyReplySubmit(c *gin.Context) {
Images: imagesData, Images: imagesData,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"), CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
} }
// 处理图片保存
go func() {
if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil { if _, err = global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").InsertOne(context.Background(), commentImages); err != nil {
global.LOG.Errorln(err) global.LOG.Errorln(err)
result.FailWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitFailed"), c)
tx.Rollback()
return
} }
}()
} }
tx.Commit() tx.Commit()
result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c) result.OkWithMessage(ginI18n.MustGetMessage(c, "CommentSubmitSuccess"), c)
@@ -388,38 +392,57 @@ func (CommentAPI) CommentList(c *gin.Context) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return return
} }
if commentListRequest.TopicId == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
query, u := gplus.NewQuery[model.ScaCommentReply]() query, u := gplus.NewQuery[model.ScaCommentReply]()
page := gplus.NewPage[model.ScaCommentReply](commentListRequest.Page, commentListRequest.Size) page := gplus.NewPage[model.ScaCommentReply](commentListRequest.Page, commentListRequest.Size)
query.Eq(&u.TopicId, commentListRequest.TopicId).Eq(&u.CommentType, enum.COMMENT).OrderByDesc(&u.Likes) query.Eq(&u.TopicId, commentListRequest.TopicId).Eq(&u.CommentType, enum.COMMENT).OrderByDesc(&u.Likes)
page, _ = gplus.SelectPage(page, query) page, _ = gplus.SelectPage(page, query)
var commentsWithImages []CommentContent userIds := make([]string, 0, len(page.Records))
for _, comment := range page.Records { for _, comment := range page.Records {
// 获取评论图片 userIds = append(userIds, comment.UserId)
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)
} }
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 // 将图片转换为base64
var imagesBase64 []string var imagesBase64 []string
for _, img := range commentImages.Images { for _, img := range commentImages.Images {
mimeType := getMimeType(img) // 动态获取 MIME 类型 mimeType := getMimeType(img)
base64Img := base64.StdEncoding.EncodeToString(img) base64Img := base64.StdEncoding.EncodeToString(img)
base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img) base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img)
imagesBase64 = append(imagesBase64, base64WithPrefix) imagesBase64 = append(imagesBase64, base64WithPrefix)
} }
// 组装评论数据 imagesBase64S[index] = imagesBase64 // 保存到切片中
queryUser, n := gplus.NewQuery[model.ScaAuthUser]()
queryUser.Eq(&n.UID, comment.UserId) userInfo := userInfoMap[comment.UserId]
userInfo, _ := gplus.SelectOne(queryUser) commentContent := CommentContent{
commentsWithImages = append(commentsWithImages,
CommentContent{
Avatar: *userInfo.Avatar, Avatar: *userInfo.Avatar,
NickName: *userInfo.Nickname, NickName: *userInfo.Nickname,
Id: comment.Id, Id: comment.Id,
@@ -435,8 +458,21 @@ func (CommentAPI) CommentList(c *gin.Context) {
Browser: comment.Browser, Browser: comment.Browser,
OperatingSystem: comment.OperatingSystem, OperatingSystem: comment.OperatingSystem,
Images: imagesBase64, 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{ response := CommentResponse{
Comments: commentsWithImages, Comments: commentsWithImages,
Size: page.Size, Size: page.Size,
@@ -447,30 +483,6 @@ func (CommentAPI) CommentList(c *gin.Context) {
return 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 获取回复列表 // ReplyList 获取回复列表
// @Summary 获取回复列表 // @Summary 获取回复列表
// @Description 获取回复列表 // @Description 获取回复列表
@@ -486,65 +498,113 @@ func (CommentAPI) ReplyList(c *gin.Context) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return return
} }
if replyListRequest.TopicId == "" || strconv.FormatInt(replyListRequest.CommentId, 10) == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
query, u := gplus.NewQuery[model.ScaCommentReply]() query, u := gplus.NewQuery[model.ScaCommentReply]()
page := gplus.NewPage[model.ScaCommentReply](replyListRequest.Page, replyListRequest.Size) 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) query.Eq(&u.TopicId, replyListRequest.TopicId).Eq(&u.ReplyId, replyListRequest.CommentId).Eq(&u.CommentType, enum.REPLY).OrderByDesc(&u.Likes)
page, _ = gplus.SelectPage(page, query) 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 { for _, comment := range page.Records {
// 获取评论图片 userIds = append(userIds, comment.UserId)
commentImages := CommentImages{} if comment.ReplyUser != "" {
wrong := global.MongoDB.Database(global.CONFIG.MongoDB.DB).Collection("comment_images").FindOne(context.Background(), bson.M{"comment_id": comment.Id}).Decode(&commentImages) replyUserIds = append(replyUserIds, comment.ReplyUser)
if wrong != nil && !errors.Is(wrong, mongo.ErrNoDocuments) {
global.LOG.Errorln(wrong)
} }
}
// 查询评论用户信息
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.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 // 将图片转换为base64
var imagesBase64 []string var imagesBase64 []string
for _, img := range commentImages.Images { for _, img := range replyImages.Images {
mimeType := getMimeType(img) // 动态获取 MIME 类型 mimeType := getMimeType(img)
base64Img := base64.StdEncoding.EncodeToString(img) base64Img := base64.StdEncoding.EncodeToString(img)
base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img) base64WithPrefix := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Img)
imagesBase64 = append(imagesBase64, base64WithPrefix) imagesBase64 = append(imagesBase64, base64WithPrefix)
} }
// 查询评论用户信息 imagesBase64S[index] = imagesBase64 // 保存到切片中
queryUser, n := gplus.NewQuery[model.ScaAuthUser]()
queryUser.Eq(&n.UID, comment.UserId) userInfo := userInfoMap[reply.UserId]
userInfo, _ := gplus.SelectOne(queryUser) replyUserInfo := replyUserInfoMap[reply.ReplyUser]
// 查询回复用户信息 commentContent := CommentContent{
queryReplyUser, m := gplus.NewQuery[model.ScaAuthUser]()
queryReplyUser.Eq(&m.UID, comment.ReplyUser)
replyUserInfo, _ := gplus.SelectOne(queryReplyUser)
commentsWithImages = append(commentsWithImages,
CommentContent{
Avatar: *userInfo.Avatar, Avatar: *userInfo.Avatar,
NickName: *userInfo.Nickname, NickName: *userInfo.Nickname,
Id: comment.Id, Id: reply.Id,
UserId: comment.UserId, UserId: reply.UserId,
TopicId: comment.TopicId, TopicId: reply.TopicId,
Dislikes: comment.Dislikes, Dislikes: reply.Dislikes,
Content: comment.Content, Content: reply.Content,
ReplyUsername: *replyUserInfo.Nickname, ReplyUsername: *replyUserInfo.Nickname,
ReplyCount: comment.ReplyCount, ReplyCount: reply.ReplyCount,
Likes: comment.Likes, Likes: reply.Likes,
CreatedTime: comment.CreatedTime, CreatedTime: reply.CreatedTime,
ReplyUser: comment.ReplyUser, ReplyUser: reply.ReplyUser,
ReplyId: comment.ReplyId, ReplyId: reply.ReplyId,
ReplyTo: comment.ReplyTo, ReplyTo: reply.ReplyTo,
Author: comment.Author, Author: reply.Author,
Location: comment.Location, Location: reply.Location,
Browser: comment.Browser, Browser: reply.Browser,
OperatingSystem: comment.OperatingSystem, OperatingSystem: reply.OperatingSystem,
Images: imagesBase64, 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{ response := CommentResponse{
Comments: commentsWithImages, Comments: repliesWithImages,
Size: page.Size, Size: page.Size,
Current: page.Current, Current: page.Current,
Total: page.Total, Total: page.Total,
@@ -552,3 +612,17 @@ func (CommentAPI) ReplyList(c *gin.Context) {
result.OkWithData(response, c) result.OkWithData(response, c)
return 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) {
}

View File

@@ -1,41 +1,47 @@
package dto package dto
type CommentRequest struct { type CommentRequest struct {
Content string `json:"content"` Content string `json:"content" binding:"required"`
Images []string `json:"images"` Images []string `json:"images"`
UserID string `json:"user_id"` UserID string `json:"user_id" binding:"required"`
TopicId string `json:"topic_id"` TopicId string `json:"topic_id" binding:"required"`
Author string `json:"author"` Author string `json:"author" binding:"required"`
} }
type ReplyCommentRequest struct { type ReplyCommentRequest struct {
Content string `json:"content"` Content string `json:"content" binding:"required"`
Images []string `json:"images"` Images []string `json:"images"`
UserID string `json:"user_id"` UserID string `json:"user_id" binding:"required"`
TopicId string `json:"topic_id"` TopicId string `json:"topic_id" binding:"required"`
ReplyId int64 `json:"reply_id"` ReplyId int64 `json:"reply_id" binding:"required"`
ReplyUser string `json:"reply_user"` ReplyUser string `json:"reply_user" binding:"required"`
Author string `json:"author"` Author string `json:"author" binding:"required"`
} }
type ReplyReplyRequest struct { type ReplyReplyRequest struct {
Content string `json:"content"` Content string `json:"content" binding:"required"`
Images []string `json:"images"` Images []string `json:"images"`
UserID string `json:"user_id"` UserID string `json:"user_id" binding:"required"`
TopicId string `json:"topic_id"` TopicId string `json:"topic_id" binding:"required"`
ReplyTo int64 `json:"reply_to"` ReplyTo int64 `json:"reply_to" binding:"required"`
ReplyId int64 `json:"reply_id"` ReplyId int64 `json:"reply_id" binding:"required"`
ReplyUser string `json:"reply_user"` ReplyUser string `json:"reply_user" binding:"required"`
Author string `json:"author"` Author string `json:"author" binding:"required""`
} }
type CommentListRequest struct { type CommentListRequest struct {
TopicId string `json:"topic_id"` TopicId string `json:"topic_id" binding:"required"`
Page int `json:"page" default:"1"` Page int `json:"page" default:"1"`
Size int `json:"size" default:"5"` Size int `json:"size" default:"5"`
} }
type ReplyListRequest struct { type ReplyListRequest struct {
TopicId string `json:"topic_id"` TopicId string `json:"topic_id" binding:"required"`
CommentId int64 `json:"comment_id"` CommentId int64 `json:"comment_id" binding:"required"`
Page int `json:"page" default:"1"` Page int `json:"page" default:"1"`
Size int `json:"size" default:"5"` 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"`
}

View File

@@ -301,11 +301,11 @@ func handelUserLogin(userId string, clientId string) bool {
resultChan <- false resultChan <- false
return return
} }
responseData := map[string]interface{}{ responseData := result.Response{
"code": 0, Data: data,
"message": "success", Message: "success",
"data": data, Code: 200,
"success": true, Success: true,
} }
tokenData, err := json.Marshal(responseData) tokenData, err := json.Marshal(responseData)
if err != nil { if err != nil {

View File

@@ -1,11 +1,11 @@
package dto package dto
type RoleRequestDto struct { type RoleRequestDto struct {
RoleName string `json:"role_name"` RoleName string `json:"role_name" binding:"required"`
RoleKey string `json:"role_key"` RoleKey string `json:"role_key" binding:"required"`
} }
type AddRoleToUserRequestDto struct { type AddRoleToUserRequestDto struct {
Uid string `json:"uid"` Uid string `json:"uid" binding:"required"`
RoleKey string `json:"role_key"` RoleKey string `json:"role_key" binding:"required"`
} }

View File

@@ -4,36 +4,36 @@ import "encoding/json"
// RefreshTokenRequest 刷新token请求 // RefreshTokenRequest 刷新token请求
type RefreshTokenRequest struct { type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token" binding:"required"`
} }
// PhoneLoginRequest 手机号登录请求 // PhoneLoginRequest 手机号登录请求
type PhoneLoginRequest struct { type PhoneLoginRequest struct {
Phone string `json:"phone"` Phone string `json:"phone" binding:"required"`
Captcha string `json:"captcha"` Captcha string `json:"captcha" binding:"required"`
AutoLogin bool `json:"auto_login"` AutoLogin bool `json:"auto_login" binding:"required"`
} }
// AccountLoginRequest 账号登录请求 // AccountLoginRequest 账号登录请求
type AccountLoginRequest struct { type AccountLoginRequest struct {
Account string `json:"account"` Account string `json:"account" binding:"required"`
Password string `json:"password"` Password string `json:"password" binding:"required"`
AutoLogin bool `json:"auto_login"` AutoLogin bool `json:"auto_login" binding:"required"`
} }
// AddUserRequest 新增用户请求 // AddUserRequest 新增用户请求
type AddUserRequest struct { type AddUserRequest struct {
Username string `json:"username"` Username string `json:"username" binding:"required"`
Password string `json:"password"` Password string `json:"password" binding:"required"`
Phone string `json:"phone"` Phone string `json:"phone" binding:"required"`
} }
// ResetPasswordRequest 重置密码请求 // ResetPasswordRequest 重置密码请求
type ResetPasswordRequest struct { type ResetPasswordRequest struct {
Phone string `json:"phone"` Phone string `json:"phone" binding:"required"`
Captcha string `json:"captcha"` Captcha string `json:"captcha" binding:"required"`
Password string `json:"password"` Password string `json:"password" binding:"required"`
Repassword string `json:"repassword"` Repassword string `json:"repassword" binding:"required"`
} }
// ResponseData 返回数据 // ResponseData 返回数据

View File

@@ -118,10 +118,6 @@ func (UserAPI) AccountLogin(c *gin.Context) {
} }
account := accountLoginRequest.Account account := accountLoginRequest.Account
password := accountLoginRequest.Password password := accountLoginRequest.Password
if account == "" || password == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "AccountAndPasswordNotEmpty"), c)
return
}
var user model.ScaAuthUser var user model.ScaAuthUser
if utils.IsPhone(account) { if utils.IsPhone(account) {
@@ -163,10 +159,6 @@ func (UserAPI) PhoneLogin(c *gin.Context) {
phone := request.Phone phone := request.Phone
captcha := request.Captcha captcha := request.Captcha
autoLogin := request.AutoLogin autoLogin := request.AutoLogin
if phone == "" || captcha == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneAndCaptchaNotEmpty"), c)
return
}
if !utils.IsPhone(phone) { if !utils.IsPhone(phone) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneErrorFormat"), c)
return return
@@ -273,10 +265,6 @@ func (UserAPI) RefreshHandler(c *gin.Context) {
return return
} }
refreshToken := request.RefreshToken refreshToken := request.RefreshToken
if refreshToken == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
parseRefreshToken, isUpd, err := utils.ParseRefreshToken(refreshToken) parseRefreshToken, isUpd, err := utils.ParseRefreshToken(refreshToken)
if err != nil || !isUpd { if err != nil || !isUpd {
global.LOG.Errorln(err) global.LOG.Errorln(err)
@@ -369,12 +357,6 @@ func (UserAPI) ResetPassword(c *gin.Context) {
captcha := resetPasswordRequest.Captcha captcha := resetPasswordRequest.Captcha
password := resetPasswordRequest.Password password := resetPasswordRequest.Password
repassword := resetPasswordRequest.Repassword repassword := resetPasswordRequest.Repassword
if phone == "" || captcha == "" || password == "" || repassword == "" {
result.FailWithMessage(ginI18n.MustGetMessage(c, "ParamsError"), c)
return
}
if !utils.IsPhone(phone) { if !utils.IsPhone(phone) {
result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c) result.FailWithMessage(ginI18n.MustGetMessage(c, "PhoneError"), c)
return return

View File

@@ -9,8 +9,9 @@ type MongoDB struct {
DB string `yaml:"db"` DB string `yaml:"db"`
User string `yaml:"user"` User string `yaml:"user"`
Password string `yaml:"password"` Password string `yaml:"password"`
MaxOpenConn int `yaml:"max-open-conn"` MaxOpenConn uint64 `yaml:"max-open-conn"`
MaxIdleConn int `yaml:"max-idle-conn"` MaxIdleConn uint64 `yaml:"max-idle-conn"`
Timeout int `yaml:"timeout"`
} }
func (m *MongoDB) MongoDsn() string { func (m *MongoDB) MongoDsn() string {

View File

@@ -2,7 +2,6 @@ package core
import ( import (
"context" "context"
"fmt"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
"schisandra-cloud-album/global" "schisandra-cloud-album/global"
@@ -24,6 +23,9 @@ func InitMongoDB() {
Password: global.CONFIG.MongoDB.Password, Password: global.CONFIG.MongoDB.Password,
PasswordSet: true, 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) connect, err := mongo.Connect(ctx, clientOptions)
if err != nil { if err != nil {
global.LOG.Fatalf(err.Error()) global.LOG.Fatalf(err.Error())
@@ -36,5 +38,4 @@ func InitMongoDB() {
return return
} }
global.MongoDB = connect global.MongoDB = connect
fmt.Println("Connected to MongoDB!")
} }

View File

@@ -5,6 +5,7 @@ const ScaCommentLikesTableName = "sca_comment_likes"
// ScaCommentLikes 评论点赞表 // ScaCommentLikes 评论点赞表
type ScaCommentLikes struct { type ScaCommentLikes struct {
Id int64 `gorm:"column:id;type:bigint(20);primary_key" json:"id"` 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"` 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"` 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"` Type int `gorm:"column:type;type:int(11);comment:类型0 点赞 1 踩;NOT NULL" json:"type"`

View File

@@ -45,7 +45,7 @@ func (CommentReplyService) GetCommentListOrderByLikesDesc(topicID uint, page, pa
func (CommentReplyService) UpdateCommentReplyCount(commentID int64) error { func (CommentReplyService) UpdateCommentReplyCount(commentID int64) error {
// 使用事务处理错误 // 使用事务处理错误
err := global.DB.Transaction(func(tx *gorm.DB) 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 { if result.Error != nil {
return result.Error // 返回更新错误 return result.Error // 返回更新错误
} }