merchant.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. package handler
  2. import (
  3. "encoding/csv"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "spider/internal/model"
  8. "spider/internal/store"
  9. "github.com/gin-gonic/gin"
  10. )
  11. // MerchantHandler handles merchant queries.
  12. type MerchantHandler struct {
  13. store *store.Store
  14. }
  15. // Stats returns aggregate statistics for merchants.
  16. func (h *MerchantHandler) Stats(c *gin.Context) {
  17. var rawTotal int64
  18. h.store.DB.Model(&model.MerchantRaw{}).Count(&rawTotal)
  19. var cleanTotal int64
  20. h.store.DB.Model(&model.MerchantClean{}).Count(&cleanTotal)
  21. // Count by status
  22. var statusRows []struct {
  23. Status string
  24. Cnt int64
  25. }
  26. h.store.DB.Model(&model.MerchantClean{}).
  27. Select("status, count(*) as cnt").
  28. Group("status").
  29. Scan(&statusRows)
  30. byStatus := map[string]int64{}
  31. for _, r := range statusRows {
  32. byStatus[r.Status] = r.Cnt
  33. }
  34. // Count by level
  35. var levelRows []struct {
  36. Level string
  37. Cnt int64
  38. }
  39. h.store.DB.Model(&model.MerchantClean{}).
  40. Select("level, count(*) as cnt").
  41. Group("level").
  42. Scan(&levelRows)
  43. byLevel := map[string]int64{}
  44. for _, r := range levelRows {
  45. byLevel[r.Level] = r.Cnt
  46. }
  47. // Count by source_type
  48. var sourceRows []struct {
  49. SourceType string
  50. Cnt int64
  51. }
  52. h.store.DB.Model(&model.MerchantRaw{}).
  53. Select("source_type, count(*) as cnt").
  54. Group("source_type").
  55. Scan(&sourceRows)
  56. bySource := map[string]int64{}
  57. for _, r := range sourceRows {
  58. bySource[r.SourceType] = r.Cnt
  59. }
  60. OK(c, gin.H{
  61. "raw_total": rawTotal,
  62. "clean_total": cleanTotal,
  63. "by_status": byStatus,
  64. "by_level": byLevel,
  65. "by_source": bySource,
  66. })
  67. }
  68. // ListRaw returns raw merchants with filters and pagination.
  69. func (h *MerchantHandler) ListRaw(c *gin.Context) {
  70. page, pageSize, offset := parsePage(c)
  71. query := h.store.DB.Model(&model.MerchantRaw{})
  72. if status := c.Query("status"); status != "" {
  73. query = query.Where("status = ?", status)
  74. }
  75. if sourceType := c.Query("source_type"); sourceType != "" {
  76. query = query.Where("source_type = ?", sourceType)
  77. }
  78. if search := c.Query("search"); search != "" {
  79. like := "%" + search + "%"
  80. query = query.Where("tg_username LIKE ? OR merchant_name LIKE ?", like, like)
  81. }
  82. var total int64
  83. query.Count(&total)
  84. var items []model.MerchantRaw
  85. if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&items).Error; err != nil {
  86. Fail(c, 500, err.Error())
  87. return
  88. }
  89. PageOK(c, items, total, page, pageSize)
  90. }
  91. // ListClean returns clean merchants with filters and pagination.
  92. func (h *MerchantHandler) ListClean(c *gin.Context) {
  93. page, pageSize, offset := parsePage(c)
  94. query := h.store.DB.Model(&model.MerchantClean{})
  95. if status := c.Query("status"); status != "" {
  96. query = query.Where("status = ?", status)
  97. }
  98. if level := c.Query("level"); level != "" {
  99. query = query.Where("level = ?", level)
  100. }
  101. if industry := c.Query("industry_tag"); industry != "" {
  102. query = query.Where("industry_tag = ?", industry)
  103. }
  104. if search := c.Query("search"); search != "" {
  105. like := "%" + search + "%"
  106. query = query.Where("tg_username LIKE ? OR merchant_name LIKE ?", like, like)
  107. }
  108. sortField := c.DefaultQuery("sort", "created_at")
  109. allowedSort := map[string]bool{
  110. "created_at": true,
  111. "updated_at": true,
  112. "source_count": true,
  113. "level": true,
  114. }
  115. if !allowedSort[sortField] {
  116. sortField = "created_at"
  117. }
  118. order := c.DefaultQuery("order", "desc")
  119. if order != "asc" && order != "desc" {
  120. order = "desc"
  121. }
  122. var total int64
  123. query.Count(&total)
  124. var items []model.MerchantClean
  125. if err := query.Order(sortField + " " + order).Limit(pageSize).Offset(offset).Find(&items).Error; err != nil {
  126. Fail(c, 500, err.Error())
  127. return
  128. }
  129. PageOK(c, items, total, page, pageSize)
  130. }
  131. // ExportCSV exports clean merchants as CSV.
  132. func (h *MerchantHandler) ExportCSV(c *gin.Context) {
  133. query := h.store.DB.Model(&model.MerchantClean{}).Where("status = ?", "valid")
  134. if level := c.Query("level"); level != "" {
  135. query = query.Where("level = ?", level)
  136. }
  137. var merchants []model.MerchantClean
  138. query.Order("level ASC, created_at DESC").Find(&merchants)
  139. c.Header("Content-Type", "text/csv; charset=utf-8")
  140. c.Header("Content-Disposition", "attachment; filename=merchants.csv")
  141. // Write BOM for Excel compatibility
  142. c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
  143. w := csv.NewWriter(c.Writer)
  144. w.Write([]string{"商户名", "TG用户名", "TG链接", "网站", "邮箱", "电话", "行业", "等级", "来源数"})
  145. for _, m := range merchants {
  146. w.Write([]string{
  147. m.MerchantName,
  148. m.TgUsername,
  149. m.TgLink,
  150. m.Website,
  151. m.Email,
  152. m.Phone,
  153. m.IndustryTag,
  154. m.Level,
  155. fmt.Sprintf("%d", m.SourceCount),
  156. })
  157. }
  158. w.Flush()
  159. }
  160. // GetByID fetches a merchant by ID.
  161. func (h *MerchantHandler) GetByID(c *gin.Context) {
  162. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  163. if err != nil {
  164. Fail(c, http.StatusBadRequest, "invalid id")
  165. return
  166. }
  167. var clean model.MerchantClean
  168. if err := h.store.DB.First(&clean, id).Error; err == nil {
  169. OK(c, gin.H{"source": "clean", "data": clean})
  170. return
  171. }
  172. var raw model.MerchantRaw
  173. if err := h.store.DB.First(&raw, id).Error; err == nil {
  174. OK(c, gin.H{"source": "raw", "data": raw})
  175. return
  176. }
  177. Fail(c, 404, "merchant not found")
  178. }