group_member_repo.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package store
  2. import (
  3. "spider/internal/model"
  4. "strings"
  5. "time"
  6. )
  7. // SaveGroupMember records a group-member relationship (idempotent via unique index).
  8. func (s *Store) SaveGroupMember(groupUsername, memberUsername, groupTitle, sourceType string, taskID uint) {
  9. groupUsername = strings.TrimPrefix(strings.TrimSpace(groupUsername), "@")
  10. memberUsername = strings.TrimPrefix(strings.TrimSpace(memberUsername), "@")
  11. if groupUsername == "" || memberUsername == "" || groupUsername == memberUsername {
  12. return
  13. }
  14. gm := model.GroupMember{
  15. GroupUsername: groupUsername,
  16. MemberUsername: memberUsername,
  17. GroupTitle: groupTitle,
  18. SourceType: sourceType,
  19. TaskID: taskID,
  20. DiscoveredAt: time.Now(),
  21. }
  22. // Use unique index for idempotency
  23. s.DB.Where("group_username = ? AND member_username = ?", groupUsername, memberUsername).
  24. FirstOrCreate(&gm)
  25. }
  26. // BatchSaveGroupMembers saves multiple group-member relationships (idempotent).
  27. // Returns the count of newly created records.
  28. func (s *Store) BatchSaveGroupMembers(groupUsername, groupTitle, sourceType string, memberUsernames []string) int {
  29. groupUsername = strings.TrimPrefix(strings.TrimSpace(groupUsername), "@")
  30. if groupUsername == "" {
  31. return 0
  32. }
  33. created := 0
  34. for _, mu := range memberUsernames {
  35. mu = strings.TrimPrefix(strings.TrimSpace(mu), "@")
  36. if mu == "" || mu == groupUsername {
  37. continue
  38. }
  39. gm := model.GroupMember{
  40. GroupUsername: groupUsername,
  41. MemberUsername: mu,
  42. GroupTitle: groupTitle,
  43. SourceType: sourceType,
  44. DiscoveredAt: time.Now(),
  45. }
  46. result := s.DB.Where("group_username = ? AND member_username = ?", groupUsername, mu).
  47. FirstOrCreate(&gm)
  48. if result.RowsAffected > 0 {
  49. created++
  50. }
  51. }
  52. return created
  53. }
  54. // ListMembersByGroup returns all members found in a group. Supports search by member username.
  55. func (s *Store) ListMembersByGroup(groupUsername string, page, pageSize int, search string) ([]model.GroupMember, int64, error) {
  56. var items []model.GroupMember
  57. var total int64
  58. q := s.DB.Model(&model.GroupMember{}).Where("group_username = ?", groupUsername)
  59. if search != "" {
  60. like := "%" + strings.TrimPrefix(strings.TrimSpace(search), "@") + "%"
  61. q = q.Where("member_username LIKE ?", like)
  62. }
  63. q.Count(&total)
  64. offset := (page - 1) * pageSize
  65. err := q.Order("discovered_at DESC").Offset(offset).Limit(pageSize).Find(&items).Error
  66. return items, total, err
  67. }
  68. // ListGroupsByMember returns all groups a member belongs to.
  69. func (s *Store) ListGroupsByMember(memberUsername string) ([]model.GroupMember, error) {
  70. var items []model.GroupMember
  71. err := s.DB.Where("member_username = ?", memberUsername).
  72. Order("discovered_at DESC").Find(&items).Error
  73. return items, err
  74. }
  75. // ListGroups returns all unique groups with member counts. Supports search by group username or title.
  76. func (s *Store) ListGroups(page, pageSize int, search string) ([]GroupSummary, int64, error) {
  77. base := s.DB.Model(&model.GroupMember{})
  78. if search != "" {
  79. like := "%" + search + "%"
  80. base = base.Where("group_username LIKE ? OR group_title LIKE ?", like, like)
  81. }
  82. var total int64
  83. base.Select("group_username").Group("group_username").Count(&total)
  84. var summaries []GroupSummary
  85. offset := (page - 1) * pageSize
  86. q := s.DB.Model(&model.GroupMember{})
  87. if search != "" {
  88. like := "%" + search + "%"
  89. q = q.Where("group_username LIKE ? OR group_title LIKE ?", like, like)
  90. }
  91. err := q.Select("group_username, MAX(group_title) as group_title, COUNT(*) as member_count, MAX(source_type) as source_type").
  92. Group("group_username").
  93. Order("member_count DESC").
  94. Offset(offset).Limit(pageSize).
  95. Scan(&summaries).Error
  96. return summaries, total, err
  97. }
  98. // SearchMembers searches for members by username across all groups.
  99. func (s *Store) SearchMembers(pattern string, page, pageSize int) ([]MemberSummary, int64, error) {
  100. like := "%" + strings.TrimPrefix(strings.TrimSpace(pattern), "@") + "%"
  101. var total int64
  102. s.DB.Model(&model.GroupMember{}).
  103. Where("member_username LIKE ?", like).
  104. Select("member_username").
  105. Group("member_username").
  106. Count(&total)
  107. var summaries []MemberSummary
  108. offset := (page - 1) * pageSize
  109. err := s.DB.Model(&model.GroupMember{}).
  110. Where("member_username LIKE ?", like).
  111. Select("member_username, COUNT(DISTINCT group_username) as group_count, MAX(discovered_at) as last_seen").
  112. Group("member_username").
  113. Order("group_count DESC").
  114. Offset(offset).Limit(pageSize).
  115. Scan(&summaries).Error
  116. return summaries, total, err
  117. }
  118. // GroupSummary is a summary of a group with member count.
  119. type GroupSummary struct {
  120. GroupUsername string `json:"group_username"`
  121. GroupTitle string `json:"group_title"`
  122. MemberCount int64 `json:"member_count"`
  123. SourceType string `json:"source_type"`
  124. }
  125. // MemberSummary is a summary of a member across groups.
  126. type MemberSummary struct {
  127. MemberUsername string `json:"member_username"`
  128. GroupCount int64 `json:"group_count"`
  129. LastSeen time.Time `json:"last_seen"`
  130. }