thread-master/apps/backend/internal/module/crm/repository/mongo.go

227 lines
5.9 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package repository
import (
"context"
libmongo "apps/backend/internal/lib/mongo"
"apps/backend/internal/module/crm/domain"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MonStore struct {
contacts *mon.Model
touches *mon.Model
followups *mon.Model
}
func NewMonStore(uri, database string) *MonStore {
uri = libmongo.MustMongoURI(uri)
return &MonStore{
contacts: mon.MustNewModel(uri, database, "crm_contacts"),
touches: mon.MustNewModel(uri, database, "crm_touches"),
followups: mon.MustNewModel(uri, database, "crm_followups"),
}
}
func (s *MonStore) UpsertContactByIdentity(ctx context.Context, c *domain.Contact) (*domain.Contact, error) {
if err := c.Normalize(); err != nil {
return nil, err
}
filter := bson.M{
"owner_uid": c.OwnerUID,
"source_platform": c.SourcePlatform,
"author_handle": c.AuthorHandle,
}
var existing domain.Contact
err := s.contacts.FindOne(ctx, &existing, filter)
if err == nil {
// merge
seen := map[string]bool{}
for _, id := range existing.OpportunityIDs {
seen[id] = true
}
for _, id := range c.OpportunityIDs {
if id != "" && !seen[id] {
existing.OpportunityIDs = append(existing.OpportunityIDs, id)
}
}
if c.TopIntentScore > existing.TopIntentScore {
existing.TopIntentScore = c.TopIntentScore
existing.TopIntentBand = c.TopIntentBand
}
existing.UpdatedAt = domain.NowNano()
_, err = s.contacts.ReplaceOne(ctx, bson.M{"_id": existing.ID}, &existing)
return &existing, err
}
if err != mon.ErrNotFound {
return nil, err
}
if c.ID == "" {
c.ID = domain.NewID()
}
now := domain.NowNano()
if c.CreatedAt == 0 {
c.CreatedAt = now
}
c.UpdatedAt = now
_, err = s.contacts.InsertOne(ctx, c)
if err != nil && mongo.IsDuplicateKeyError(err) {
return s.UpsertContactByIdentity(ctx, c)
}
return c, err
}
func (s *MonStore) GetContact(ctx context.Context, id string) (*domain.Contact, error) {
var c domain.Contact
err := s.contacts.FindOne(ctx, &c, bson.M{"_id": id})
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
if err != nil {
return nil, err
}
return &c, nil
}
func (s *MonStore) SaveContact(ctx context.Context, c *domain.Contact) error {
_, err := s.contacts.ReplaceOne(ctx, bson.M{"_id": c.ID}, c, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) ListContacts(ctx context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, error) {
q := bson.M{"owner_uid": ownerUID}
if f.Stage != "" {
q["stage"] = f.Stage
}
if f.FollowUp != nil {
q["needs_follow_up"] = *f.FollowUp
}
if f.Band != "" {
q["top_intent_band"] = f.Band
}
total, err := s.contacts.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
}
sortKey := "last_touch_at"
if f.Sort == "intent_score" {
sortKey = "top_intent_score"
}
var list []*domain.Contact
err = s.contacts.Find(ctx, &list, q, options.Find().
SetSort(bson.D{{Key: sortKey, Value: -1}}).
SetSkip(int64((page-1)*ps)).
SetLimit(int64(ps)))
return list, total, err
}
func (s *MonStore) CountByStage(ctx context.Context, ownerUID int64) (map[string]int, error) {
// simple: list all and count (contacts per owner are bounded)
list, _, err := s.ListContacts(ctx, ownerUID, domain.ContactListFilter{Page: 1, PageSize: 500})
if err != nil {
return nil, err
}
out := map[string]int{}
follow := 0
for _, c := range list {
out[c.Stage]++
if c.NeedsFollowUp {
follow++
}
}
out["needs_follow_up"] = follow
return out, nil
}
func (s *MonStore) InsertTouch(ctx context.Context, t *domain.ContactTouch) error {
if t.ID == "" {
t.ID = domain.NewID()
}
_, err := s.touches.InsertOne(ctx, t)
return err
}
func (s *MonStore) ListTouches(ctx context.Context, ownerUID int64, contactID string, page, pageSize int) ([]*domain.ContactTouch, int64, error) {
q := bson.M{"owner_uid": ownerUID, "contact_id": contactID}
total, err := s.touches.CountDocuments(ctx, q)
if err != nil {
return nil, 0, err
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
var list []*domain.ContactTouch
err = s.touches.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) SaveFollowUp(ctx context.Context, f *domain.FollowUp) error {
_, err := s.followups.ReplaceOne(ctx, bson.M{"_id": f.ID}, f, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) GetFollowUp(ctx context.Context, id string) (*domain.FollowUp, error) {
var f domain.FollowUp
err := s.followups.FindOne(ctx, &f, bson.M{"_id": id})
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
if err != nil {
return nil, err
}
return &f, nil
}
func (s *MonStore) ListFollowUps(ctx context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
q := bson.M{"owner_uid": ownerUID}
if f.Status != "" {
q["status"] = f.Status
}
total, err := s.followups.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.FollowUp
err = s.followups.Find(ctx, &list, q, options.Find().
SetSort(bson.D{{Key: "due_at", Value: 1}}).
SetSkip(int64((page-1)*ps)).
SetLimit(int64(ps)))
return list, total, err
}
func (s *MonStore) ListDueFollowUps(ctx context.Context, now int64, limit int) ([]*domain.FollowUp, error) {
if limit <= 0 {
limit = 50
}
var list []*domain.FollowUp
err := s.followups.Find(ctx, &list, bson.M{
"status": bson.M{"$in": []string{domain.FollowUpScheduled, domain.FollowUpSnoozed}},
"due_at": bson.M{"$lte": now},
}, options.Find().SetLimit(int64(limit)))
return list, err
}