package scan_post import ( "context" "math" "sort" "haixun-backend/internal/library/authctx" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" scanpostusecase "haixun-backend/internal/model/scan_post/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" "github.com/zeromicro/go-zero/core/logx" ) type LookalikeLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewLookalikeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LookalikeLogic { return &LookalikeLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx} } func (l *LookalikeLogic) Lookalike(req *types.LookalikeReq) (*types.LookalikeData, error) { actor, ok := authctx.ActorFromContext(l.ctx) if !ok { return nil, app.For(code.Auth).AuthUnauthorized("missing actor") } source, err := l.svcCtx.ScanPost.Get(l.ctx, actor.TenantID, actor.UID, req.BrandID, req.SourcePostID) if err != nil { return nil, err } if len(source.Embedding) == 0 { return &types.LookalikeData{List: nil}, nil } brandID := req.BrandID if brandID == "" { brandID = source.BrandID } limit := req.Limit if limit < 1 { limit = 10 } if limit > 50 { limit = 50 } candidates, err := l.svcCtx.ScanPost.List(l.ctx, scanpostusecase.ListRequest{ TenantID: actor.TenantID, OwnerUID: actor.UID, BrandID: brandID, Limit: 200, }) if err != nil { return nil, err } type scored struct { post scanpostusecase.ScanPostSummary sim float64 } var scoredList []scored for _, c := range candidates { if c.ID == req.SourcePostID { continue } if len(c.Embedding) == 0 { continue } sim := cosineSimilarity(source.Embedding, c.Embedding) if sim < 0.3 { continue } scoredList = append(scoredList, scored{post: c, sim: sim}) } sort.Slice(scoredList, func(i, j int) bool { return scoredList[i].sim > scoredList[j].sim }) if len(scoredList) > limit { scoredList = scoredList[:limit] } out := make([]types.LookalikeItemData, 0, len(scoredList)) for _, s := range scoredList { out = append(out, types.LookalikeItemData{ PostID: s.post.ID, AuthorID: s.post.AuthorID, AuthorName: s.post.Author, AuthorAvatar: s.post.AuthorAvatar, Permalink: s.post.Permalink, Text: s.post.Text, PlacementScore: s.post.PlacementScore, SemanticScore: s.post.SemanticScore, EngagementPredicted: s.post.EngagementPredicted, AudienceQuality: s.post.AudienceQualityScore, Similarity: math.Round(s.sim*1000) / 1000, }) } return &types.LookalikeData{List: out}, nil } func cosineSimilarity(a, b []float32) float64 { if len(a) == 0 || len(b) == 0 || len(a) != len(b) { return 0 } var dot, na, nb float64 for i := range a { dot += float64(a[i]) * float64(b[i]) na += float64(a[i]) * float64(a[i]) nb += float64(b[i]) * float64(b[i]) } if na == 0 || nb == 0 { return 0 } return dot / (math.Sqrt(na) * math.Sqrt(nb)) }