fix: 发现流补上已有机器人的短帖
发现列表之前没有机器人帖。打开广场发现时,给已有机器人各写一条原创短帖。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cfa30e270b
commit
d604cffe84
@ -55,7 +55,7 @@ func (s *Service) PublishBotMedia(ctx context.Context, userID int64, in *BotPubl
|
|||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
post := &entity.Post{
|
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,
|
Visibility: entity.VisibilityPublic, AuditStatus: entity.AuditApproved,
|
||||||
Status: entity.StatusNormal, Source: entity.SourceBotRSS, SourceURL: url,
|
Status: entity.StatusNormal, Source: entity.SourceBotRSS, SourceURL: url,
|
||||||
ExpiresAtMS: now.Add(ttl).UnixMilli(), CreatedAtMS: now.UnixMilli(),
|
ExpiresAtMS: now.Add(ttl).UnixMilli(), CreatedAtMS: now.UnixMilli(),
|
||||||
|
|||||||
93
internal/module/square/logic/discover_bots.go
Normal file
93
internal/module/square/logic/discover_bots.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
54
internal/module/square/logic/discover_bots_test.go
Normal file
54
internal/module/square/logic/discover_bots_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
@ -27,6 +27,7 @@ func (s *Service) Feed(ctx context.Context, viewerID int64, channel entity.Chann
|
|||||||
if !q.Category.Valid() {
|
if !q.Category.Valid() {
|
||||||
return nil, errs.ErrInvalidArgument
|
return nil, errs.ErrInvalidArgument
|
||||||
}
|
}
|
||||||
|
s.ensureDiscoverBots(ctx, channel, q.Category)
|
||||||
limit := entity.ClampLimit(q.Limit)
|
limit := entity.ClampLimit(q.Limit)
|
||||||
picked := make([]*entity.Post, 0, limit)
|
picked := make([]*entity.Post, 0, limit)
|
||||||
scan := q.Cursor
|
scan := q.Cursor
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package logic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
friendentity "spark-server/internal/module/friend/entity"
|
friendentity "spark-server/internal/module/friend/entity"
|
||||||
"spark-server/internal/module/square/entity"
|
"spark-server/internal/module/square/entity"
|
||||||
@ -49,6 +51,7 @@ type Friends interface {
|
|||||||
// Users loads profile cards.
|
// Users loads profile cards.
|
||||||
type Users interface {
|
type Users interface {
|
||||||
Get(ctx context.Context, id int64) (*userentity.User, error)
|
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.
|
// Objects deletes square media. Nil skips GC.
|
||||||
@ -71,6 +74,8 @@ type Service struct {
|
|||||||
burst *burst
|
burst *burst
|
||||||
media media.Policy
|
media media.Policy
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
|
discoverMu sync.Mutex
|
||||||
|
discoverReady atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(repo Repository, friends Friends, users Users, objects Objects, ids IDGen) *Service {
|
func NewService(repo Repository, friends Friends, users Users, objects Objects, ids IDGen) *Service {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user