package repository import ( "context" libmongo "apps/backend/internal/lib/mongo" "apps/backend/internal/module/growth/domain" "github.com/zeromicro/go-zero/core/stores/mon" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) type MonStore struct { outcomes *mon.Model checkups *mon.Model health *mon.Model workspaces *mon.Model wsState *mon.Model reviews *mon.Model rewards *mon.Model playbooks *mon.Model utm *mon.Model wsMembers *mon.Model bench *mon.Model } func NewMonStore(uri, database string) *MonStore { uri = libmongo.MustMongoURI(uri) return &MonStore{ outcomes: mon.MustNewModel(uri, database, "growth_outcomes"), checkups: mon.MustNewModel(uri, database, "growth_checkups"), health: mon.MustNewModel(uri, database, "growth_account_health"), workspaces: mon.MustNewModel(uri, database, "growth_workspaces"), wsState: mon.MustNewModel(uri, database, "growth_ws_state"), reviews: mon.MustNewModel(uri, database, "growth_draft_reviews"), rewards: mon.MustNewModel(uri, database, "growth_invite_rewards"), playbooks: mon.MustNewModel(uri, database, "growth_playbooks"), utm: mon.MustNewModel(uri, database, "growth_utm_links"), wsMembers: mon.MustNewModel(uri, database, "growth_ws_members"), bench: mon.MustNewModel(uri, database, "growth_benchmark"), } } func (s *MonStore) UpsertOutcomeBySource(ctx context.Context, e *domain.OutcomeEvent) error { filter := bson.M{"owner_uid": e.OwnerUID, "source_type": e.SourceType, "source_id": e.SourceID} var existing domain.OutcomeEvent err := s.outcomes.FindOne(ctx, &existing, filter) if err == nil { // 發佈 hook 可能重送;已存在就保持原 sent_at 與所有觀測/成交訊號。 *e = existing return nil } if err != mon.ErrNotFound { return err } if e.ID == "" { e.ID = domain.NewID() } _, err = s.outcomes.InsertOne(ctx, e) return err } func (s *MonStore) GetOutcome(ctx context.Context, id string) (*domain.OutcomeEvent, error) { var e domain.OutcomeEvent err := s.outcomes.FindOne(ctx, &e, bson.M{"_id": id}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } if err != nil { return nil, err } return &e, nil } func (s *MonStore) ListOutcomes(ctx context.Context, ownerUID int64, f domain.OutcomeListFilter) ([]*domain.OutcomeEvent, int64, error) { q := bson.M{"owner_uid": ownerUID} if f.Kind != "" { q["kind"] = f.Kind } if f.Confidence != "" { q["confidence"] = f.Confidence } if f.SourceType != "" { q["source_type"] = f.SourceType } if f.From > 0 || f.To > 0 { rangeQ := bson.M{} if f.From > 0 { rangeQ["$gte"] = f.From } if f.To > 0 { rangeQ["$lte"] = f.To } q["sent_at"] = rangeQ } total, err := s.outcomes.CountDocuments(ctx, q) if err != nil { return nil, 0, err } page, ps := f.Page, f.PageSize if page < 1 { page = 1 } if ps < 1 { ps = 20 } var list []*domain.OutcomeEvent err = s.outcomes.Find(ctx, &list, q, options.Find(). SetSort(bson.D{{Key: "sent_at", Value: -1}}). SetSkip(int64((page-1)*ps)). SetLimit(int64(ps))) return list, total, err } func (s *MonStore) SaveOutcome(ctx context.Context, e *domain.OutcomeEvent) error { _, err := s.outcomes.UpdateOne(ctx, bson.M{"_id": e.ID}, bson.M{"$set": e}, options.Update().SetUpsert(true)) return err } func (s *MonStore) ListObservingDue(ctx context.Context, now int64, limit int) ([]*domain.OutcomeEvent, error) { var list []*domain.OutcomeEvent opts := options.Find().SetSort(bson.D{{Key: "sent_at", Value: 1}}) if limit > 0 { opts.SetLimit(int64(limit)) } err := s.outcomes.Find(ctx, &list, bson.M{"status": domain.StatusObserving}, opts) return list, err } func (s *MonStore) SaveCheckup(ctx context.Context, c *domain.WeeklyCheckup) error { _, err := s.checkups.UpdateOne(ctx, bson.M{"_id": c.ID}, bson.M{"$set": c}, options.Update().SetUpsert(true)) return err } func (s *MonStore) GetCheckup(ctx context.Context, id string) (*domain.WeeklyCheckup, error) { var c domain.WeeklyCheckup err := s.checkups.FindOne(ctx, &c, bson.M{"_id": id}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &c, err } func (s *MonStore) LatestCheckup(ctx context.Context, ownerUID int64) (*domain.WeeklyCheckup, error) { var list []*domain.WeeklyCheckup err := s.checkups.Find(ctx, &list, bson.M{"owner_uid": ownerUID}, options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}).SetLimit(1)) if err != nil { return nil, err } if len(list) == 0 { return nil, domain.ErrNotFound } return list[0], nil } func (s *MonStore) ListCheckups(ctx context.Context, ownerUID int64, page, pageSize int) ([]*domain.WeeklyCheckup, int64, error) { q := bson.M{"owner_uid": ownerUID} total, err := s.checkups.CountDocuments(ctx, q) if err != nil { return nil, 0, err } if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } var list []*domain.WeeklyCheckup err = s.checkups.Find(ctx, &list, q, options.Find(). SetSort(bson.D{{Key: "created_at", Value: -1}}). SetSkip(int64((page-1)*pageSize)).SetLimit(int64(pageSize))) return list, total, err } func (s *MonStore) CountCheckupGenerations(ctx context.Context, ownerUID int64, sinceNano int64) (int64, error) { return s.checkups.CountDocuments(ctx, bson.M{"owner_uid": ownerUID, "created_at": bson.M{"$gte": sinceNano}}) } func (s *MonStore) SaveHealth(ctx context.Context, h *domain.AccountHealth) error { _, err := s.health.UpdateOne(ctx, bson.M{"_id": h.ThreadsAccountID}, bson.M{"$set": h}, options.Update().SetUpsert(true)) return err } func (s *MonStore) GetHealth(ctx context.Context, accountID string) (*domain.AccountHealth, error) { var h domain.AccountHealth err := s.health.FindOne(ctx, &h, bson.M{"_id": accountID}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &h, err } func (s *MonStore) ListHealth(ctx context.Context, ownerUID int64) ([]*domain.AccountHealth, error) { var list []*domain.AccountHealth err := s.health.Find(ctx, &list, bson.M{"owner_uid": ownerUID}) return list, err } func (s *MonStore) SaveWorkspace(ctx context.Context, w *domain.Workspace) error { _, err := s.workspaces.UpdateOne(ctx, bson.M{"_id": w.ID}, bson.M{"$set": w}, options.Update().SetUpsert(true)) return err } func (s *MonStore) GetWorkspace(ctx context.Context, id string) (*domain.Workspace, error) { var w domain.Workspace err := s.workspaces.FindOne(ctx, &w, bson.M{"_id": id}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &w, err } func (s *MonStore) ListWorkspaces(ctx context.Context, ownerUID int64, includeArchived bool) ([]*domain.Workspace, error) { q := bson.M{"owner_uid": ownerUID} if !includeArchived { q["archived"] = bson.M{"$ne": true} } var list []*domain.Workspace err := s.workspaces.Find(ctx, &list, q, options.Find().SetSort(bson.D{{Key: "created_at", Value: 1}})) return list, err } func (s *MonStore) CountActiveWorkspaces(ctx context.Context, ownerUID int64) (int64, error) { return s.workspaces.CountDocuments(ctx, bson.M{"owner_uid": ownerUID, "archived": bson.M{"$ne": true}}) } func (s *MonStore) GetMemberWSState(ctx context.Context, ownerUID int64) (*domain.MemberWorkspaceState, error) { var st domain.MemberWorkspaceState err := s.wsState.FindOne(ctx, &st, bson.M{"_id": ownerUID}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &st, err } func (s *MonStore) SaveMemberWSState(ctx context.Context, st *domain.MemberWorkspaceState) error { _, err := s.wsState.UpdateOne(ctx, bson.M{"_id": st.OwnerUID}, bson.M{"$set": st}, options.Update().SetUpsert(true)) return err } func (s *MonStore) SaveReview(ctx context.Context, r *domain.DraftReview) error { _, err := s.reviews.UpdateOne(ctx, bson.M{"_id": r.ID}, bson.M{"$set": r}, options.Update().SetUpsert(true)) return err } func (s *MonStore) GetReview(ctx context.Context, id string) (*domain.DraftReview, error) { var r domain.DraftReview err := s.reviews.FindOne(ctx, &r, bson.M{"_id": id}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &r, err } func (s *MonStore) FindReviewByRef(ctx context.Context, ownerUID int64, refType, refID string) (*domain.DraftReview, error) { var list []*domain.DraftReview err := s.reviews.Find(ctx, &list, bson.M{"owner_uid": ownerUID, "ref_type": refType, "ref_id": refID}, options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}}).SetLimit(1)) if err != nil { return nil, err } if len(list) == 0 { return nil, domain.ErrNotFound } return list[0], nil } func (s *MonStore) ListReviews(ctx context.Context, ownerUID int64, status string, page, pageSize int) ([]*domain.DraftReview, int64, error) { q := bson.M{"owner_uid": ownerUID} if status != "" { q["status"] = status } total, err := s.reviews.CountDocuments(ctx, q) if err != nil { return nil, 0, err } if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } var list []*domain.DraftReview err = s.reviews.Find(ctx, &list, q, options.Find().SetSkip(int64((page-1)*pageSize)).SetLimit(int64(pageSize))) return list, total, err } func (s *MonStore) SaveInviteReward(ctx context.Context, r *domain.InviteReward) error { _, err := s.rewards.InsertOne(ctx, r) return err } func (s *MonStore) ListInviteRewards(ctx context.Context, inviterUID int64, limit int) ([]*domain.InviteReward, error) { opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}) if limit > 0 { opts.SetLimit(int64(limit)) } var list []*domain.InviteReward err := s.rewards.Find(ctx, &list, bson.M{"inviter_uid": inviterUID}, opts) return list, err } func (s *MonStore) SumInviteRewardPointsMonth(ctx context.Context, inviterUID int64, monthStartNano int64) (int, error) { var list []*domain.InviteReward err := s.rewards.Find(ctx, &list, bson.M{ "inviter_uid": inviterUID, "status": domain.RewardCredited, "created_at": bson.M{"$gte": monthStartNano}, }) if err != nil { return 0, err } sum := 0 for _, r := range list { sum += r.Points } return sum, nil } func (s *MonStore) SumInviteRewardPointsTotal(ctx context.Context, inviterUID int64) (int, error) { var list []*domain.InviteReward err := s.rewards.Find(ctx, &list, bson.M{"inviter_uid": inviterUID, "status": domain.RewardCredited}) if err != nil { return 0, err } sum := 0 for _, r := range list { sum += r.Points } return sum, nil } func (s *MonStore) HasInviteRewardForInvitee(ctx context.Context, inviteeUID int64) (bool, error) { n, err := s.rewards.CountDocuments(ctx, bson.M{ "invitee_uid": inviteeUID, "status": bson.M{"$in": []string{domain.RewardCredited, domain.RewardCapped}}, }) return n > 0, err } var _ domain.Repository = (*MonStore)(nil) func (s *MonStore) SavePlaybook(ctx context.Context, p *domain.Playbook) error { _, err := s.playbooks.UpdateOne(ctx, bson.M{"_id": p.ID}, bson.M{"$set": p}, options.Update().SetUpsert(true)) return err } func (s *MonStore) GetPlaybook(ctx context.Context, id string) (*domain.Playbook, error) { var p domain.Playbook err := s.playbooks.FindOne(ctx, &p, bson.M{"_id": id}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &p, err } func (s *MonStore) ListPlaybooks(ctx context.Context, kind, niche string, ownerOnlyUID int64, page, pageSize int) ([]*domain.Playbook, int64, error) { q := bson.M{} if kind != "" { q["kind"] = kind } if niche != "" { q["niche"] = bson.M{"$regex": niche, "$options": "i"} } if ownerOnlyUID > 0 { q["owner_uid"] = ownerOnlyUID } total, err := s.playbooks.CountDocuments(ctx, q) if err != nil { return nil, 0, err } if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } var list []*domain.Playbook err = s.playbooks.Find(ctx, &list, q, options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}).SetSkip(int64((page-1)*pageSize)).SetLimit(int64(pageSize))) return list, total, err } func (s *MonStore) DeletePlaybook(ctx context.Context, id string) error { res, err := s.playbooks.DeleteOne(ctx, bson.M{"_id": id}) if err != nil { return err } if res == 0 { return domain.ErrNotFound } return nil } func (s *MonStore) IncPlaybookImport(ctx context.Context, id string) error { _, err := s.playbooks.UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$inc": bson.M{"import_count": 1}}) return err } func (s *MonStore) SaveUtmLink(ctx context.Context, u *domain.UtmLink) error { _, err := s.utm.UpdateOne(ctx, bson.M{"_id": u.ID}, bson.M{"$set": u}, options.Update().SetUpsert(true)) return err } func (s *MonStore) GetUtmByCode(ctx context.Context, code string) (*domain.UtmLink, error) { var u domain.UtmLink err := s.utm.FindOne(ctx, &u, bson.M{"code": code}) if err == mon.ErrNotFound { return nil, domain.ErrNotFound } return &u, err } func (s *MonStore) ListUtmLinks(ctx context.Context, ownerUID int64) ([]*domain.UtmLink, error) { var list []*domain.UtmLink err := s.utm.Find(ctx, &list, bson.M{"owner_uid": ownerUID}, options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})) return list, err } func (s *MonStore) IncUtmClick(ctx context.Context, code string) (*domain.UtmLink, error) { _, err := s.utm.UpdateOne(ctx, bson.M{"code": code}, bson.M{"$inc": bson.M{"clicks": 1}}) if err != nil { return nil, err } return s.GetUtmByCode(ctx, code) } func (s *MonStore) SaveWorkspaceMember(ctx context.Context, m *domain.WorkspaceMember) error { _, err := s.wsMembers.UpdateOne(ctx, bson.M{"_id": m.ID}, bson.M{"$set": m}, options.Update().SetUpsert(true)) return err } func (s *MonStore) ListWorkspaceMembers(ctx context.Context, workspaceID string) ([]*domain.WorkspaceMember, error) { var list []*domain.WorkspaceMember err := s.wsMembers.Find(ctx, &list, bson.M{"workspace_id": workspaceID}) return list, err } func (s *MonStore) RemoveWorkspaceMember(ctx context.Context, workspaceID string, uid int64) error { res, err := s.wsMembers.DeleteOne(ctx, bson.M{"workspace_id": workspaceID, "uid": uid}) if err != nil { return err } if res == 0 { return domain.ErrNotFound } return nil } func (s *MonStore) IsWorkspaceMember(ctx context.Context, workspaceID string, uid int64) (bool, string, error) { w, err := s.GetWorkspace(ctx, workspaceID) if err == nil && w.OwnerUID == uid { return true, domain.WSRoleOwner, nil } var list []*domain.WorkspaceMember err = s.wsMembers.Find(ctx, &list, bson.M{"workspace_id": workspaceID, "uid": uid}, options.Find().SetLimit(1)) if err != nil { return false, "", err } if len(list) == 0 { return false, "", nil } return true, list[0].Role, nil } func (s *MonStore) UpsertBenchmarkSample(ctx context.Context, sample *domain.BenchmarkSample) error { if sample.ID == "" { sample.ID = sample.Niche + ":" + sample.OwnerHash } _, err := s.bench.UpdateOne(ctx, bson.M{"_id": sample.ID}, bson.M{"$set": sample}, options.Update().SetUpsert(true)) return err } func (s *MonStore) ListBenchmarkSamples(ctx context.Context, niche string, limit int) ([]*domain.BenchmarkSample, error) { q := bson.M{} if niche != "" { q["niche"] = niche } opts := options.Find() if limit > 0 { opts.SetLimit(int64(limit)) } var list []*domain.BenchmarkSample err := s.bench.Find(ctx, &list, q, opts) return list, err }