group.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package handler
  2. import (
  3. "context"
  4. "encoding/csv"
  5. "fmt"
  6. "time"
  7. "spider/internal/store"
  8. "spider/internal/telegram"
  9. "github.com/gin-gonic/gin"
  10. "github.com/redis/go-redis/v9"
  11. )
  12. // GroupHandler handles group-member relationship queries.
  13. type GroupHandler struct {
  14. store *store.Store
  15. tgManager *telegram.AccountManager
  16. rdb *redis.Client
  17. }
  18. // ListGroups handles GET /groups — returns all groups with member counts.
  19. func (h *GroupHandler) ListGroups(c *gin.Context) {
  20. page, pageSize, _ := parsePage(c)
  21. search := c.Query("search")
  22. groups, total, err := h.store.ListGroups(page, pageSize, search)
  23. if err != nil {
  24. Fail(c, 500, err.Error())
  25. return
  26. }
  27. PageOK(c, groups, total, page, pageSize)
  28. }
  29. // SearchMembers handles GET /members/search — search members across all groups.
  30. func (h *GroupHandler) SearchMembers(c *gin.Context) {
  31. query := c.Query("q")
  32. if query == "" {
  33. Fail(c, 400, "搜索关键词不能为空")
  34. return
  35. }
  36. page, pageSize, _ := parsePage(c)
  37. members, total, err := h.store.SearchMembers(query, page, pageSize)
  38. if err != nil {
  39. Fail(c, 500, err.Error())
  40. return
  41. }
  42. PageOK(c, members, total, page, pageSize)
  43. }
  44. // ListMembers handles GET /groups/:username/members — members of a group.
  45. func (h *GroupHandler) ListMembers(c *gin.Context) {
  46. username := c.Param("username")
  47. search := c.Query("search")
  48. page, pageSize, _ := parsePage(c)
  49. members, total, err := h.store.ListMembersByGroup(username, page, pageSize, search)
  50. if err != nil {
  51. Fail(c, 500, err.Error())
  52. return
  53. }
  54. PageOK(c, members, total, page, pageSize)
  55. }
  56. // ListMemberGroups handles GET /merchants/:username/groups — groups a member belongs to.
  57. func (h *GroupHandler) ListMemberGroups(c *gin.Context) {
  58. username := c.Param("username")
  59. groups, err := h.store.ListGroupsByMember(username)
  60. if err != nil {
  61. Fail(c, 500, err.Error())
  62. return
  63. }
  64. OK(c, groups)
  65. }
  66. // ExportMembers handles GET /groups/:username/members/export — streams all members as CSV.
  67. func (h *GroupHandler) ExportMembers(c *gin.Context) {
  68. username := c.Param("username")
  69. members, _, err := h.store.ListMembersByGroup(username, 1, 100000, "")
  70. if err != nil {
  71. Fail(c, 500, err.Error())
  72. return
  73. }
  74. c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="members_%s.csv"`, username))
  75. c.Header("Content-Type", "text/csv; charset=utf-8")
  76. c.Writer.Write([]byte("\xef\xbb\xbf")) // UTF-8 BOM for Excel
  77. w := csv.NewWriter(c.Writer)
  78. _ = w.Write([]string{"用户名", "来源类型", "发现时间"})
  79. for _, m := range members {
  80. discoveredAt := ""
  81. if !m.DiscoveredAt.IsZero() {
  82. discoveredAt = m.DiscoveredAt.Format("2006-01-02 15:04:05")
  83. }
  84. _ = w.Write([]string{m.MemberUsername, m.SourceType, discoveredAt})
  85. }
  86. w.Flush()
  87. }
  88. // CloneMembers handles POST /groups/:username/clone-members
  89. // Runs a multi-account, FloodWait-aware, resumable clone. Progress lives in
  90. // Redis so a call that hits "all accounts cooling" can return Partial=true
  91. // and be retried later to continue where it left off.
  92. // Query param ?reset=1 discards prior progress and restarts from scratch.
  93. func (h *GroupHandler) CloneMembers(c *gin.Context) {
  94. username := c.Param("username")
  95. if username == "" {
  96. Fail(c, 400, "群组用户名不能为空")
  97. return
  98. }
  99. if h.tgManager == nil {
  100. Fail(c, 500, "TG 账号管理器未初始化")
  101. return
  102. }
  103. if h.rdb == nil {
  104. Fail(c, 500, "Redis 客户端未初始化")
  105. return
  106. }
  107. reset := c.Query("reset") == "1"
  108. ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Minute)
  109. defer cancel()
  110. res, err := telegram.CloneGroupMembers(ctx, h.tgManager, h.rdb, username, reset)
  111. if err != nil && res == nil {
  112. Fail(c, 500, fmt.Sprintf("克隆失败: %v", err))
  113. return
  114. }
  115. if res == nil {
  116. Fail(c, 500, "克隆返回空结果")
  117. return
  118. }
  119. // Filter + persist all currently-known participants with usernames.
  120. var usernames []string
  121. for _, p := range res.Participants {
  122. if p.Username != "" && !p.IsBot {
  123. usernames = append(usernames, p.Username)
  124. }
  125. }
  126. groupTitle := res.GroupTitle
  127. if groupTitle == "" {
  128. groupTitle = username
  129. }
  130. created := 0
  131. if len(usernames) > 0 {
  132. created = h.store.BatchSaveGroupMembers(username, groupTitle, "tg_clone", usernames)
  133. }
  134. LogAudit(h.store, c, "clone_members", "group", username, gin.H{
  135. "total_participants": len(res.Participants),
  136. "with_username": len(usernames),
  137. "new_saved": created,
  138. "partial": res.Partial,
  139. "status": res.Status,
  140. "queries_done": res.QueriesDone,
  141. "queries_total": res.QueriesTotal,
  142. "reset": reset,
  143. })
  144. OK(c, gin.H{
  145. "group_username": username,
  146. "group_title": groupTitle,
  147. "total_participants": len(res.Participants),
  148. "with_username": len(usernames),
  149. "new_saved": created,
  150. "partial": res.Partial,
  151. "status": res.Status,
  152. "progress": gin.H{
  153. "collected": len(res.Participants),
  154. "total": res.Total,
  155. "queries_done": res.QueriesDone,
  156. "queries_total": res.QueriesTotal,
  157. },
  158. })
  159. }