merchant.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package handler
  2. import (
  3. "net/http"
  4. "strconv"
  5. "spider/internal/model"
  6. "github.com/gin-gonic/gin"
  7. "gorm.io/gorm"
  8. )
  9. // MerchantHandler handles merchant queries.
  10. type MerchantHandler struct {
  11. db *gorm.DB
  12. }
  13. // Stats returns aggregate statistics for merchants.
  14. // GET /merchants/stats
  15. func (h *MerchantHandler) Stats(c *gin.Context) {
  16. type countRow struct {
  17. Key string `json:"key"`
  18. Count int64 `json:"count"`
  19. }
  20. var rawTotal int64
  21. h.db.Model(&model.MerchantRaw{}).Count(&rawTotal)
  22. var cleanTotal int64
  23. h.db.Model(&model.MerchantClean{}).Count(&cleanTotal)
  24. // Count by status in clean table.
  25. statusCounts := map[string]int64{}
  26. var statusRows []struct {
  27. Status string
  28. Cnt int64
  29. }
  30. h.db.Model(&model.MerchantClean{}).
  31. Select("status, count(*) as cnt").
  32. Group("status").
  33. Scan(&statusRows)
  34. for _, r := range statusRows {
  35. statusCounts[r.Status] = r.Cnt
  36. }
  37. // Count by source_type in raw table.
  38. var sourceRows []struct {
  39. SourceType string
  40. Cnt int64
  41. }
  42. h.db.Model(&model.MerchantRaw{}).
  43. Select("source_type, count(*) as cnt").
  44. Group("source_type").
  45. Scan(&sourceRows)
  46. bySource := map[string]int64{}
  47. for _, r := range sourceRows {
  48. bySource[r.SourceType] = r.Cnt
  49. }
  50. // Count by industry in clean table.
  51. var industryRows []struct {
  52. Industry string
  53. Cnt int64
  54. }
  55. h.db.Model(&model.MerchantClean{}).
  56. Select("industry, count(*) as cnt").
  57. Group("industry").
  58. Scan(&industryRows)
  59. byIndustry := map[string]int64{}
  60. for _, r := range industryRows {
  61. byIndustry[r.Industry] = r.Cnt
  62. }
  63. OK(c, gin.H{
  64. "raw_total": rawTotal,
  65. "clean_total": cleanTotal,
  66. "valid": statusCounts["valid"],
  67. "invalid": statusCounts["invalid"],
  68. "bot": statusCounts["bot"],
  69. "duplicate": statusCounts["duplicate"],
  70. "group": statusCounts["group"],
  71. "by_source": bySource,
  72. "by_industry": byIndustry,
  73. })
  74. }
  75. // ListRaw returns raw merchants with filters and pagination.
  76. // GET /merchants/raw?status=&source_type=&page=&page_size=
  77. func (h *MerchantHandler) ListRaw(c *gin.Context) {
  78. page, pageSize, offset := parsePage(c)
  79. query := h.db.Model(&model.MerchantRaw{})
  80. if status := c.Query("status"); status != "" {
  81. query = query.Where("status = ?", status)
  82. }
  83. if sourceType := c.Query("source_type"); sourceType != "" {
  84. query = query.Where("source_type = ?", sourceType)
  85. }
  86. var total int64
  87. if err := query.Count(&total).Error; err != nil {
  88. Fail(c, 500, err.Error())
  89. return
  90. }
  91. var items []model.MerchantRaw
  92. if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&items).Error; err != nil {
  93. Fail(c, 500, err.Error())
  94. return
  95. }
  96. PageOK(c, items, total, page, pageSize)
  97. }
  98. // ListClean returns clean merchants with filters and pagination.
  99. // GET /merchants/clean?status=&industry=&min_score=&sort=quality_score&order=desc&page=&page_size=
  100. func (h *MerchantHandler) ListClean(c *gin.Context) {
  101. page, pageSize, offset := parsePage(c)
  102. query := h.db.Model(&model.MerchantClean{})
  103. if status := c.Query("status"); status != "" {
  104. query = query.Where("status = ?", status)
  105. }
  106. if industry := c.Query("industry"); industry != "" {
  107. query = query.Where("industry = ?", industry)
  108. }
  109. if minScore := c.Query("min_score"); minScore != "" {
  110. if score, err := strconv.ParseFloat(minScore, 64); err == nil {
  111. query = query.Where("quality_score >= ?", score)
  112. }
  113. }
  114. sortField := c.DefaultQuery("sort", "quality_score")
  115. // whitelist sort fields to prevent SQL injection
  116. allowedSort := map[string]bool{
  117. "quality_score": true,
  118. "created_at": true,
  119. "updated_at": true,
  120. "member_count": true,
  121. }
  122. if !allowedSort[sortField] {
  123. sortField = "quality_score"
  124. }
  125. order := c.DefaultQuery("order", "desc")
  126. if order != "asc" && order != "desc" {
  127. order = "desc"
  128. }
  129. var total int64
  130. if err := query.Count(&total).Error; err != nil {
  131. Fail(c, 500, err.Error())
  132. return
  133. }
  134. var items []model.MerchantClean
  135. if err := query.Order(sortField + " " + order).Limit(pageSize).Offset(offset).Find(&items).Error; err != nil {
  136. Fail(c, 500, err.Error())
  137. return
  138. }
  139. PageOK(c, items, total, page, pageSize)
  140. }
  141. // GetByID fetches a merchant by ID, checking clean table first then raw.
  142. // GET /merchants/:id
  143. func (h *MerchantHandler) GetByID(c *gin.Context) {
  144. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  145. if err != nil {
  146. Fail(c, http.StatusBadRequest, "invalid id")
  147. return
  148. }
  149. var clean model.MerchantClean
  150. if err := h.db.First(&clean, id).Error; err == nil {
  151. OK(c, gin.H{"source": "clean", "data": clean})
  152. return
  153. }
  154. var raw model.MerchantRaw
  155. if err := h.db.First(&raw, id).Error; err == nil {
  156. OK(c, gin.H{"source": "raw", "data": raw})
  157. return
  158. }
  159. Fail(c, 404, "merchant not found")
  160. }