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

278 lines
6.5 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package repository
import (
"context"
"fmt"
"sort"
"sync"
"apps/backend/internal/module/crm/domain"
)
type Memory struct {
mu sync.Mutex
contacts map[string]*domain.Contact
identity map[string]string // owner|platform|handle → id
touches map[string]*domain.ContactTouch
followups map[string]*domain.FollowUp
}
func NewMemory() *Memory {
return &Memory{
contacts: map[string]*domain.Contact{},
identity: map[string]string{},
touches: map[string]*domain.ContactTouch{},
followups: map[string]*domain.FollowUp{},
}
}
func idKey(owner int64, platform, handle string) string {
return fmt.Sprintf("%d|%s|%s", owner, platform, handle)
}
func (m *Memory) UpsertContactByIdentity(_ context.Context, c *domain.Contact) (*domain.Contact, error) {
if err := c.Normalize(); err != nil {
return nil, err
}
m.mu.Lock()
defer m.mu.Unlock()
key := idKey(c.OwnerUID, c.SourcePlatform, c.AuthorHandle)
if id, ok := m.identity[key]; ok {
ex := m.contacts[id]
// merge opportunity ids
seen := map[string]bool{}
for _, x := range ex.OpportunityIDs {
seen[x] = true
}
for _, x := range c.OpportunityIDs {
if x != "" && !seen[x] {
ex.OpportunityIDs = append(ex.OpportunityIDs, x)
}
}
if c.TopIntentScore > ex.TopIntentScore {
ex.TopIntentScore = c.TopIntentScore
ex.TopIntentBand = c.TopIntentBand
}
ex.UpdatedAt = domain.NowNano()
cp := *ex
return &cp, nil
}
if c.ID == "" {
c.ID = domain.NewID()
}
now := domain.NowNano()
if c.CreatedAt == 0 {
c.CreatedAt = now
}
c.UpdatedAt = now
cp := *c
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
m.contacts[cp.ID] = &cp
m.identity[key] = cp.ID
out := cp
out.OpportunityIDs = append([]string(nil), cp.OpportunityIDs...)
return &out, nil
}
func (m *Memory) GetContact(_ context.Context, id string) (*domain.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
c, ok := m.contacts[id]
if !ok {
return nil, domain.ErrNotFound
}
cp := *c
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
cp.MergedFrom = append([]string(nil), c.MergedFrom...)
return &cp, nil
}
func (m *Memory) SaveContact(_ context.Context, c *domain.Contact) error {
m.mu.Lock()
defer m.mu.Unlock()
cp := *c
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
cp.MergedFrom = append([]string(nil), c.MergedFrom...)
m.contacts[c.ID] = &cp
m.identity[idKey(c.OwnerUID, c.SourcePlatform, c.AuthorHandle)] = c.ID
return nil
}
func (m *Memory) ListContacts(_ context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
matched := make([]*domain.Contact, 0)
for _, c := range m.contacts {
if c.OwnerUID != ownerUID {
continue
}
if f.Stage != "" && c.Stage != f.Stage {
continue
}
if f.FollowUp != nil && c.NeedsFollowUp != *f.FollowUp {
continue
}
if f.Band != "" && c.TopIntentBand != f.Band {
continue
}
cp := *c
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
matched = append(matched, &cp)
}
sort.Slice(matched, func(i, j int) bool {
if f.Sort == "intent_score" {
if matched[i].TopIntentScore != matched[j].TopIntentScore {
return matched[i].TopIntentScore > matched[j].TopIntentScore
}
}
if matched[i].LastTouchAt != matched[j].LastTouchAt {
return matched[i].LastTouchAt > matched[j].LastTouchAt
}
return matched[i].CreatedAt > matched[j].CreatedAt
})
total := int64(len(matched))
page, ps := f.Page, f.PageSize
if page < 1 {
page = 1
}
if ps < 1 {
ps = 20
}
start := (page - 1) * ps
if start >= len(matched) {
return nil, total, nil
}
end := start + ps
if end > len(matched) {
end = len(matched)
}
return matched[start:end], total, nil
}
func (m *Memory) CountByStage(_ context.Context, ownerUID int64) (map[string]int, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := map[string]int{}
follow := 0
for _, c := range m.contacts {
if c.OwnerUID != ownerUID {
continue
}
out[c.Stage]++
if c.NeedsFollowUp {
follow++
}
}
out["needs_follow_up"] = follow
return out, nil
}
func (m *Memory) InsertTouch(_ context.Context, t *domain.ContactTouch) error {
m.mu.Lock()
defer m.mu.Unlock()
if t.ID == "" {
t.ID = domain.NewID()
}
cp := *t
m.touches[t.ID] = &cp
return nil
}
func (m *Memory) ListTouches(_ context.Context, ownerUID int64, contactID string, page, pageSize int) ([]*domain.ContactTouch, int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
matched := make([]*domain.ContactTouch, 0)
for _, t := range m.touches {
if t.OwnerUID == ownerUID && t.ContactID == contactID {
cp := *t
matched = append(matched, &cp)
}
}
sort.Slice(matched, func(i, j int) bool { return matched[i].CreatedAt > matched[j].CreatedAt })
total := int64(len(matched))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
start := (page - 1) * pageSize
if start >= len(matched) {
return nil, total, nil
}
end := start + pageSize
if end > len(matched) {
end = len(matched)
}
return matched[start:end], total, nil
}
func (m *Memory) SaveFollowUp(_ context.Context, f *domain.FollowUp) error {
m.mu.Lock()
defer m.mu.Unlock()
cp := *f
m.followups[f.ID] = &cp
return nil
}
func (m *Memory) GetFollowUp(_ context.Context, id string) (*domain.FollowUp, error) {
m.mu.Lock()
defer m.mu.Unlock()
f, ok := m.followups[id]
if !ok {
return nil, domain.ErrNotFound
}
cp := *f
return &cp, nil
}
func (m *Memory) ListFollowUps(_ context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
matched := make([]*domain.FollowUp, 0)
for _, x := range m.followups {
if x.OwnerUID != ownerUID {
continue
}
if f.Status != "" && x.Status != f.Status {
continue
}
cp := *x
matched = append(matched, &cp)
}
sort.Slice(matched, func(i, j int) bool { return matched[i].DueAt < matched[j].DueAt })
total := int64(len(matched))
page, ps := f.Page, f.PageSize
if page < 1 {
page = 1
}
if ps < 1 {
ps = 20
}
start := (page - 1) * ps
if start >= len(matched) {
return nil, total, nil
}
end := start + ps
if end > len(matched) {
end = len(matched)
}
return matched[start:end], total, nil
}
func (m *Memory) ListDueFollowUps(_ context.Context, now int64, limit int) ([]*domain.FollowUp, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]*domain.FollowUp, 0)
for _, x := range m.followups {
if (x.Status == domain.FollowUpScheduled || x.Status == domain.FollowUpSnoozed) && x.DueAt <= now {
cp := *x
out = append(out, &cp)
}
}
if limit > 0 && len(out) > limit {
out = out[:limit]
}
return out, nil
}