spark-server/internal/module/seed/logic/service.go
DESKTOP-L8OUL9F\gfl0034 ce2cdf6bf3 feat: music 运营曲目、seed 机器人欢迎消息,并补充 novels/music OpenAPI
合并 origin/main 设备注册后,纳入本地 music 模块、seed 命令与 popadmin 相关路由/文档;music migration 编号为 0020 以避免与 0019_devices 冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 16:07:05 +08:00

141 lines
4.2 KiB
Go

// Package logic seeds demo bots, welcome messages, and the English sample group.
package logic
import (
"context"
"errors"
"fmt"
friendentity "spark-server/internal/module/friend/entity"
groupentity "spark-server/internal/module/group/entity"
msgentity "spark-server/internal/module/message/entity"
userentity "spark-server/internal/module/user/entity"
usermodel "spark-server/internal/module/user/model"
"spark-server/internal/pkg/errs"
)
// IDGen issues Snowflake ids for users, conversations, and messages.
type IDGen interface {
NextID() (int64, error)
}
// Users loads and creates accounts used by the seed.
type Users interface {
GetByEmail(ctx context.Context, email string) (*userentity.User, error)
CreateWithEmail(ctx context.Context, u *userentity.User, email string) error
ListAccounts(ctx context.Context) ([]usermodel.Account, error)
}
// Friends writes directed friend edges.
type Friends interface {
GetRelation(ctx context.Context, userID, friendID int64) (*friendentity.Relation, error)
AcceptFriend(ctx context.Context, in friendentity.AcceptParams) error
}
// Chats opens P2P conversations and bumps last_msg.
type Chats interface {
EnsureP2P(ctx context.Context, userA, userB int64) (int64, error)
UpdateLastMsg(ctx context.Context, conversationID, msgID, atMS int64) error
}
// Messages persists private / group bodies.
type Messages interface {
Create(ctx context.Context, m *msgentity.Message) error
GetByClientMsgID(ctx context.Context, senderID, conversationID int64, clientMsgID string) (*msgentity.Message, error)
}
// Groups creates the sample group and membership.
type Groups interface {
FindByOwnerAndName(ctx context.Context, ownerID int64, name string) (*groupentity.Group, error)
CreateGroup(ctx context.Context, g *groupentity.Group, members []*groupentity.Member) error
GetMember(ctx context.Context, groupID, userID int64) (*groupentity.Member, error)
AddMembers(ctx context.Context, groupID int64, members []*groupentity.Member) error
UpdateLastMsg(ctx context.Context, conversationID, msgID, atMS int64) error
}
// Service is the idempotent demo / onboarding seeder.
type Service struct {
users Users
friends Friends
chats Chats
msgs Messages
groups Groups
ids IDGen
}
// NewService wires seed dependencies.
func NewService(users Users, friends Friends, chats Chats, msgs Messages, groups Groups, ids IDGen) *Service {
return &Service{users: users, friends: friends, chats: chats, msgs: msgs, groups: groups, ids: ids}
}
// Result summarizes what the seed pass did (created or already present).
type Result struct {
Bots int `json:"bots"`
TestUsers int `json:"test_users"`
Targets int `json:"targets"`
GroupID int64 `json:"group_id"`
SampleMsgs int `json:"sample_msgs"`
}
func (s *Service) ready() bool {
return s != nil && s.users != nil && s.friends != nil && s.chats != nil &&
s.msgs != nil && s.groups != nil && s.ids != nil
}
// Run ensures bots, test accounts, per-user welcome/greeting, and the sample group.
func (s *Service) Run(ctx context.Context) (*Result, error) {
if !s.ready() {
return nil, errs.ErrAvailable
}
bots, err := s.ensureBots(ctx)
if err != nil {
return nil, err
}
if err := s.ensureTestUsers(ctx); err != nil {
return nil, err
}
targets, err := s.listTargets(ctx)
if err != nil {
return nil, err
}
welcome := bots[userentity.WelcomeBotIndex-1]
for _, u := range targets {
if err := s.onboardUser(ctx, u, welcome); err != nil {
return nil, fmt.Errorf("onboard %d: %w", u.ID, err)
}
}
gid, samples, err := s.ensureSampleGroup(ctx, bots, targets)
if err != nil {
return nil, err
}
return &Result{
Bots: len(bots), TestUsers: 10, Targets: len(targets),
GroupID: gid, SampleMsgs: samples,
}, nil
}
func (s *Service) listTargets(ctx context.Context) ([]*userentity.User, error) {
accounts, err := s.users.ListAccounts(ctx)
if err != nil {
return nil, err
}
out := make([]*userentity.User, 0, len(accounts))
for _, a := range accounts {
if a.User == nil || userentity.IsBotEmail(a.Email) {
continue
}
if a.User.Status != userentity.StatusActive {
continue
}
out = append(out, a.User)
}
return out, nil
}
func ignoreDup(err error) error {
if err == nil || errors.Is(err, errs.ErrDuplicate) {
return nil
}
return err
}