package logic import ( "context" "strconv" "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 PublishInput struct { Text string `json:"text" validate:"omitempty,max=1000" label:"field.square_text" errmsg:"max:validate.square.text_invalid"` Images []*entity.Image `json:"images" validate:"omitempty,max=9,dive"` Video *entity.Video `json:"video"` Visibility entity.Visibility `json:"visibility" validate:"omitempty,oneof=0 1 2 3" label:"field.square_visibility" errmsg:"oneof:validate.square.visibility_invalid"` AudienceUIDs []int64 `json:"audience_uids" validate:"omitempty,max=200,dive,gt=0" label:"field.square_audience"` POI *entity.POI `json:"poi"` Category entity.Category `json:"category" validate:"omitempty,oneof=funny beauty" label:"field.square_category" errmsg:"oneof:validate.square.category_invalid"` } func (s *Service) Publish(ctx context.Context, userID int64, channel entity.Channel, in *PublishInput) (*View, error) { if !s.ready() || userID <= 0 { return nil, errs.ErrUnauthorized } channel = normalizeChannel(channel) if !channel.Valid() { return nil, errs.ErrInvalidArgument } if in == nil { return nil, errs.ErrInvalidArgument } if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil { return nil, err } if in.Category != "" && !in.Category.IsTag() { return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareCategoryInvalid, nil)) } if !s.allowPublish(ctx, userID) { return nil, errs.ErrRateLimited } content, kind, err := buildContent(ctx, in) if err != nil { return nil, err } if err := s.checkMediaMeta(ctx, in.Images, in.Video); err != nil { return nil, err } raw, err := content.Marshal() if err != nil { return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareContentInvalid, nil)) } if err := s.checkAudience(ctx, userID, in); err != nil { return nil, err } text, err := s.gateText(ctx, userID, strings.TrimSpace(in.Text)) if err != nil { return nil, err } id, err := s.ids.NextID() if err != nil { return nil, err } now := time.Now() vis := in.Visibility if !vis.Valid() { vis = entity.VisibilityPublic } post := &entity.Post{ ID: id, UserID: userID, Channel: channel, Category: in.Category, Kind: kind, Text: text, Content: raw, Visibility: vis, AuditStatus: entity.AuditApproved, Status: entity.StatusNormal, CreatedAtMS: now.UnixMilli(), } if in.POI != nil { post.POI = *in.POI } if err := s.repo.CreatePost(ctx, post, uniqueIDs(in.AudienceUIDs)); err != nil { return nil, err } return s.viewOf(ctx, userID, post, true) } func (s *Service) allowPublish(ctx context.Context, userID int64) bool { key := strconv.FormatInt(userID, 10) + ":" + reqctx.RealIP(ctx) return s.burst.allow("p1:"+key, time.Minute, 2) && s.burst.allow("pd:"+key, 24*time.Hour, 20) } func (s *Service) allowInteract(ctx context.Context, userID int64) bool { key := strconv.FormatInt(userID, 10) + ":" + reqctx.RealIP(ctx) return s.burst.allow("i:"+key, time.Minute, 60) } func buildContent(ctx context.Context, in *PublishInput) (*entity.Content, entity.Kind, error) { lang := reqctx.Lang(ctx) text := strings.TrimSpace(in.Text) if utf8.RuneCountInString(text) > entity.MaxTextRunes { return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareTextInvalid, nil)) } if len(in.Images) > 0 && in.Video != nil { return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareMediaConflict, nil)) } if text == "" && len(in.Images) == 0 && in.Video == nil { return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareEmpty, nil)) } c := &entity.Content{Images: in.Images, Video: in.Video} if err := c.Validate(); err != nil { return nil, 0, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareContentInvalid, nil)) } return c, c.Kind(), nil } func (s *Service) checkAudience(ctx context.Context, userID int64, in *PublishInput) error { lang := reqctx.Lang(ctx) if !in.Visibility.Valid() { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareVisibilityInvalid, nil)) } uids := uniqueIDs(in.AudienceUIDs) if !in.Visibility.NeedsAudience() { if len(uids) > 0 { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil)) } return nil } if len(uids) == 0 || len(uids) > entity.MaxAudience { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil)) } for _, peer := range uids { if peer == userID { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil)) } ok, err := s.friends.AreFriends(ctx, userID, peer) if err != nil { return err } if !ok { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeySquareAudienceInvalid, nil)) } } return nil } func uniqueIDs(ids []int64) []int64 { seen := make(map[int64]struct{}, len(ids)) out := make([]int64, 0, len(ids)) for _, id := range ids { if id <= 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out }