spark-server/internal/module/square/logic/bot_publish.go
DESKTOP-L8OUL9F\gfl0034 d604cffe84 fix: 发现流补上已有机器人的短帖
发现列表之前没有机器人帖。打开广场发现时,给已有机器人各写一条原创短帖。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 22:40:51 +08:00

89 lines
2.6 KiB
Go

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"
)
// BotPublishInput is an internal publish from the media bot worker (no rate limit).
// Images must already be compressed and stored under perm/square/image/.
type BotPublishInput struct {
Text string
SourceURL string
TTL time.Duration
Images []*entity.Image
}
// PublishBotMedia publishes a public post as a bot user with TTL and URL dedupe.
func (s *Service) PublishBotMedia(ctx context.Context, userID int64, in *BotPublishInput) (*View, error) {
if !s.ready() || userID <= 0 {
return nil, errs.ErrUnauthorized
}
if in == nil {
return nil, errs.ErrInvalidArgument
}
text := strings.TrimSpace(in.Text)
url := strings.TrimSpace(in.SourceURL)
if text == "" || url == "" {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareEmpty, nil))
}
if utf8.RuneCountInString(text) > entity.MaxTextRunes {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareTextInvalid, nil))
}
ttl := in.TTL
if ttl <= 0 {
ttl = 15 * 24 * time.Hour
}
content := &entity.Content{Images: in.Images}
if err := content.Validate(); err != nil {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareContentInvalid, nil))
}
raw, err := content.Marshal()
if err != nil {
return nil, errs.ErrInvalidArgument.WithMsg(i18n.T(reqctx.Lang(ctx), i18n.KeySquareContentInvalid, nil))
}
id, err := s.ids.NextID()
if err != nil {
return nil, err
}
now := time.Now()
post := &entity.Post{
ID: id, UserID: userID, Channel: entity.ChannelSquare, Kind: content.Kind(), Text: text, Content: raw,
Visibility: entity.VisibilityPublic, AuditStatus: entity.AuditApproved,
Status: entity.StatusNormal, Source: entity.SourceBotRSS, SourceURL: url,
ExpiresAtMS: now.Add(ttl).UnixMilli(), CreatedAtMS: now.UnixMilli(),
}
if err := s.repo.CreateBotPost(ctx, post); err != nil {
return nil, err
}
return s.viewOf(ctx, userID, post, true)
}
// ExpireBotPosts soft-deletes expired bot posts and best-effort deletes media.
func (s *Service) ExpireBotPosts(ctx context.Context, now time.Time, limit int) (int, error) {
if !s.ready() {
return 0, nil
}
if now.IsZero() {
now = time.Now()
}
if limit <= 0 {
limit = 200
}
posts, err := s.repo.SoftDeleteExpiredBotPosts(ctx, now.UnixMilli(), limit)
if err != nil {
return 0, err
}
for _, post := range posts {
s.deleteMedia(ctx, post)
}
return len(posts), nil
}