From d604cffe846c0eada5205e6d949b2a5f0fa759af Mon Sep 17 00:00:00 2001 From: "DESKTOP-L8OUL9F\\gfl0034" <1003614916@qq.com> Date: Sun, 18 Oct 2026 22:40:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8F=91=E7=8E=B0=E6=B5=81=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=E5=B7=B2=E6=9C=89=E6=9C=BA=E5=99=A8=E4=BA=BA=E7=9A=84?= =?UTF-8?q?=E7=9F=AD=E5=B8=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发现列表之前没有机器人帖。打开广场发现时,给已有机器人各写一条原创短帖。 Co-authored-by: Cursor --- internal/module/square/logic/bot_publish.go | 2 +- internal/module/square/logic/discover_bots.go | 93 +++++++++++++++++++ .../module/square/logic/discover_bots_test.go | 54 +++++++++++ internal/module/square/logic/feed.go | 1 + internal/module/square/logic/service.go | 21 +++-- 5 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 internal/module/square/logic/discover_bots.go create mode 100644 internal/module/square/logic/discover_bots_test.go diff --git a/internal/module/square/logic/bot_publish.go b/internal/module/square/logic/bot_publish.go index b19ac31..8056e86 100644 --- a/internal/module/square/logic/bot_publish.go +++ b/internal/module/square/logic/bot_publish.go @@ -55,7 +55,7 @@ func (s *Service) PublishBotMedia(ctx context.Context, userID int64, in *BotPubl } now := time.Now() post := &entity.Post{ - ID: id, UserID: userID, Kind: content.Kind(), Text: text, Content: raw, + 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(), diff --git a/internal/module/square/logic/discover_bots.go b/internal/module/square/logic/discover_bots.go new file mode 100644 index 0000000..4a89c86 --- /dev/null +++ b/internal/module/square/logic/discover_bots.go @@ -0,0 +1,93 @@ +package logic + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "go.uber.org/zap" + + "spark-server/internal/module/square/entity" + userentity "spark-server/internal/module/user/entity" + "spark-server/internal/pkg/errs" +) + +const discoverBotURLPrefix = "https://spark.wyrs365.com/square/discover/bot/" + +// ensureDiscoverBots puts one original short post from each existing robot +// onto the square discovery feed. It does not fetch or copy outside articles. +func (s *Service) ensureDiscoverBots(ctx context.Context, channel entity.Channel, category entity.Category) { + if s == nil || channel != entity.ChannelSquare || category.IsTag() || s.discoverReady.Load() { + return + } + s.discoverMu.Lock() + defer s.discoverMu.Unlock() + if s.discoverReady.Load() { + return + } + if err := s.fillDiscoverBots(ctx); err != nil { + if s.log != nil { + s.log.Warn("square discover bots", zap.Error(err)) + } + return + } + s.discoverReady.Store(true) +} + +func (s *Service) fillDiscoverBots(ctx context.Context) error { + var failed bool + for i := 1; i <= userentity.BotCount; i++ { + email := userentity.BotEmail(i) + user, err := s.users.GetByEmail(ctx, email) + if err != nil { + if errors.Is(err, errs.ErrNotFound) { + continue + } + failed = true + continue + } + if user == nil || user.ID <= 0 || user.Status != userentity.StatusActive { + continue + } + _, err = s.PublishBotMedia(ctx, user.ID, &BotPublishInput{ + Text: discoverBotText(i, user.Nickname), + SourceURL: discoverBotURL(i), + TTL: 15 * 24 * time.Hour, + }) + if err == nil || errors.Is(err, errs.ErrDuplicate) { + continue + } + failed = true + } + if failed { + return errs.ErrInner + } + return nil +} + +func discoverBotURL(index int) string { + if index < 1 { + return "" + } + return fmt.Sprintf("%s%03d", discoverBotURLPrefix, index) +} + +func discoverBotText(index int, nickname string) string { + name := strings.TrimSpace(nickname) + if name == "" { + name = fmt.Sprintf("bot%03d", index) + } + lines := []string{ + "%s:把今天看到的光记在发现里。", + "%s:路过广场,留一句给后来的人。", + "%s:这条是给发现页的,不跟上一句重复。", + "%s:风很小,适合把一句话放在这里。", + "%s:发现页空着的时候,补一条短的。", + "%s:同一条不连发,换个人再看下一句。", + "%s:文字先到,配图要有合法的对象键才附上。", + "%s:推荐流里见,不占别的广场频道。", + } + return fmt.Sprintf(lines[(index-1)%len(lines)], name) +} diff --git a/internal/module/square/logic/discover_bots_test.go b/internal/module/square/logic/discover_bots_test.go new file mode 100644 index 0000000..be71e5c --- /dev/null +++ b/internal/module/square/logic/discover_bots_test.go @@ -0,0 +1,54 @@ +package logic + +import ( + "context" + "strings" + "testing" + + "spark-server/internal/module/square/entity" + userentity "spark-server/internal/module/user/entity" + userlogic "spark-server/internal/module/user/logic" +) + +func TestDiscoverFeedReturnsExistingBotPost(t *testing.T) { + svc, _, users := testSquare(t) + ctx := t.Context() + bot := &userentity.User{ID: 88001, Nickname: "小光", Status: userentity.StatusActive, SparkID: "sparkbot001"} + svc.users = &discoverUsers{inner: users, bot: bot} + viewer, err := users.AuthenticateEmail(ctx, "test01@spark.com", "1", &userentity.LoginDevice{DeviceID: 1, DeviceType: userentity.DeviceIOS}) + if err != nil { + t.Fatal(err) + } + viewCtx := ctxUser(t, viewer.ID, "10.0.0.8") + out, err := svc.Feed(viewCtx, viewer.ID, entity.ChannelSquare, FeedQuery{Category: entity.CategoryRecommend, Limit: 20}) + if err != nil || len(out.Items) != 1 { + t.Fatalf("items %d %v", len(out.Items), err) + } + item := out.Items[0] + if item.User.UID != bot.ID || item.Channel != entity.ChannelSquare || !strings.Contains(item.Text, "发现") { + t.Fatalf("post %+v", item) + } + again, err := svc.Feed(viewCtx, viewer.ID, entity.ChannelSquare, FeedQuery{Category: entity.CategoryRecommend, Limit: 20}) + if err != nil || len(again.Items) != 1 || again.Items[0].ID != item.ID { + t.Fatalf("repeat %+v %v", again, err) + } +} + +type discoverUsers struct { + inner *userlogic.Service + bot *userentity.User +} + +func (d *discoverUsers) Get(ctx context.Context, id int64) (*userentity.User, error) { + if d.bot != nil && id == d.bot.ID { + return d.bot, nil + } + return d.inner.Get(ctx, id) +} + +func (d *discoverUsers) GetByEmail(ctx context.Context, email string) (*userentity.User, error) { + if email == "bot001@spark.bot" { + return d.bot, nil + } + return d.inner.GetByEmail(ctx, email) +} diff --git a/internal/module/square/logic/feed.go b/internal/module/square/logic/feed.go index 15944c5..fe18dc6 100644 --- a/internal/module/square/logic/feed.go +++ b/internal/module/square/logic/feed.go @@ -27,6 +27,7 @@ func (s *Service) Feed(ctx context.Context, viewerID int64, channel entity.Chann if !q.Category.Valid() { return nil, errs.ErrInvalidArgument } + s.ensureDiscoverBots(ctx, channel, q.Category) limit := entity.ClampLimit(q.Limit) picked := make([]*entity.Post, 0, limit) scan := q.Cursor diff --git a/internal/module/square/logic/service.go b/internal/module/square/logic/service.go index d9e24e5..2aea09c 100644 --- a/internal/module/square/logic/service.go +++ b/internal/module/square/logic/service.go @@ -2,6 +2,8 @@ package logic import ( "context" + "sync" + "sync/atomic" friendentity "spark-server/internal/module/friend/entity" "spark-server/internal/module/square/entity" @@ -49,6 +51,7 @@ type Friends interface { // Users loads profile cards. type Users interface { Get(ctx context.Context, id int64) (*userentity.User, error) + GetByEmail(ctx context.Context, email string) (*userentity.User, error) } // Objects deletes square media. Nil skips GC. @@ -63,14 +66,16 @@ type IDGen interface { // Service is the square use-case layer. type Service struct { - repo Repository - friends Friends - users Users - objects Objects - ids IDGen - burst *burst - media media.Policy - log *zap.Logger + repo Repository + friends Friends + users Users + objects Objects + ids IDGen + burst *burst + media media.Policy + log *zap.Logger + discoverMu sync.Mutex + discoverReady atomic.Bool } func NewService(repo Repository, friends Friends, users Users, objects Objects, ids IDGen) *Service {