package logic import ( "context" "strings" "time" "unicode/utf8" "spark-server/internal/module/square/entity" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/i18n" "spark-server/internal/pkg/reqctx" "spark-server/internal/pkg/validate" ) type CommentInput struct { Text string `json:"text" validate:"required,min=1,max=500" label:"field.square_comment" errmsg:"required:validate.square.comment_required;min:validate.square.comment_invalid;max:validate.square.comment_invalid"` ReplyToID int64 `json:"reply_to_id" validate:"omitempty,gt=0" label:"field.square_reply"` } func (s *Service) Comment(ctx context.Context, userID, postID int64, in *CommentInput) (*View, error) { post, err := s.visiblePost(ctx, userID, postID) if err != nil { return nil, err } if in == nil { return nil, errs.ErrInvalidArgument } if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil { return nil, err } if !s.allowInteract(ctx, userID) { return nil, errs.ErrRateLimited } text := strings.TrimSpace(in.Text) if text == "" || utf8.RuneCountInString(text) > entity.MaxCommentRunes { return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareCommentInvalid, nil)) } parent, err := s.loadParent(ctx, postID, in.ReplyToID) if err != nil { return nil, err } id, err := s.ids.NextID() if err != nil { return nil, err } now := time.Now().UnixMilli() row := &entity.Comment{ ID: id, PostID: postID, UserID: userID, Text: text, Status: entity.StatusNormal, CreatedAtMS: now, } if parent != nil { row.ReplyToID = parent.ID row.ReplyToUID = parent.UserID } if err := s.repo.AddComment(ctx, row); err != nil { return nil, err } _ = s.notify(ctx, commentNotices(post, row, userID)) return s.Get(ctx, userID, postID) } func (s *Service) DeleteComment(ctx context.Context, userID, postID, commentID int64) (*View, error) { post, err := s.visiblePost(ctx, userID, postID) if err != nil { return nil, err } c, err := s.repo.GetComment(ctx, commentID) if err != nil { return nil, err } if c.PostID != postID || c.Status == entity.StatusDeleted { return nil, errs.ErrNotFound } if c.UserID != userID && post.UserID != userID { return nil, errs.ErrForbidden } if err := s.repo.SoftDeleteComment(ctx, commentID); err != nil { return nil, err } return s.Get(ctx, userID, postID) } func (s *Service) loadParent(ctx context.Context, postID, replyToID int64) (*entity.Comment, error) { if replyToID <= 0 { return nil, nil } parent, err := s.repo.GetComment(ctx, replyToID) if err != nil { return nil, err } if parent.PostID != postID || parent.Status != entity.StatusNormal { return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareReplyInvalid, nil)) } return parent, nil } func commentNotices(post *entity.Post, c *entity.Comment, from int64) []*entity.Notice { seen := map[int64]struct{}{} out := make([]*entity.Notice, 0, 2) add := func(to int64, kind entity.NoticeKind) { if to <= 0 || to == from { return } if _, ok := seen[to]; ok { return } seen[to] = struct{}{} out = append(out, &entity.Notice{ ToUserID: to, FromUserID: from, Kind: kind, PostID: post.ID, CommentID: c.ID, }) } if c.ReplyToUID > 0 { add(c.ReplyToUID, entity.NoticeReply) } add(post.UserID, entity.NoticeComment) return out }