keyword.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package handler
  2. import (
  3. "encoding/csv"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "spider/internal/model"
  10. "spider/internal/store"
  11. "github.com/gin-gonic/gin"
  12. )
  13. // KeywordHandler handles unified keyword + seed CRUD.
  14. type KeywordHandler struct {
  15. store *store.Store
  16. }
  17. // List returns keywords with optional filters and pagination.
  18. // GET /keywords?page=1&page_size=20&industry_tag=
  19. func (h *KeywordHandler) List(c *gin.Context) {
  20. page, pageSize, offset := parsePage(c)
  21. industryTag := c.Query("industry_tag")
  22. query := h.store.DB.Model(&model.Keyword{})
  23. if industryTag != "" {
  24. query = query.Where("industry_tag = ?", industryTag)
  25. }
  26. var total int64
  27. query.Count(&total)
  28. var keywords []model.Keyword
  29. if err := query.Order("id DESC").Limit(pageSize).Offset(offset).Find(&keywords).Error; err != nil {
  30. Fail(c, 500, err.Error())
  31. return
  32. }
  33. PageOK(c, keywords, total, page, pageSize)
  34. }
  35. // Create creates one or more keywords in batch.
  36. // POST /keywords body: {keywords:["k1","k2"], industry_tag:"机场"}
  37. func (h *KeywordHandler) Create(c *gin.Context) {
  38. var body struct {
  39. Keywords []string `json:"keywords" binding:"required,min=1"`
  40. IndustryTag string `json:"industry_tag"`
  41. }
  42. if err := c.ShouldBindJSON(&body); err != nil {
  43. Fail(c, http.StatusBadRequest, err.Error())
  44. return
  45. }
  46. var created []model.Keyword
  47. for _, kw := range body.Keywords {
  48. if kw == "" {
  49. continue
  50. }
  51. k := model.Keyword{
  52. Keyword: kw,
  53. IndustryTag: body.IndustryTag,
  54. Enabled: true,
  55. }
  56. if err := h.store.DB.Where(model.Keyword{Keyword: kw}).FirstOrCreate(&k).Error; err != nil {
  57. Fail(c, 500, err.Error())
  58. return
  59. }
  60. created = append(created, k)
  61. }
  62. LogAudit(h.store, c, "create", "keyword", "", gin.H{"count": len(created)})
  63. OK(c, created)
  64. }
  65. // Update modifies a keyword.
  66. // PUT /keywords/:id
  67. func (h *KeywordHandler) Update(c *gin.Context) {
  68. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  69. if err != nil {
  70. Fail(c, http.StatusBadRequest, "invalid id")
  71. return
  72. }
  73. var body struct {
  74. Keyword string `json:"keyword"`
  75. IndustryTag string `json:"industry_tag"`
  76. Enabled *bool `json:"enabled"`
  77. }
  78. if err := c.ShouldBindJSON(&body); err != nil {
  79. Fail(c, http.StatusBadRequest, err.Error())
  80. return
  81. }
  82. var kw model.Keyword
  83. if err := h.store.DB.First(&kw, id).Error; err != nil {
  84. Fail(c, 404, "keyword not found")
  85. return
  86. }
  87. updates := map[string]any{}
  88. if body.Keyword != "" {
  89. updates["keyword"] = body.Keyword
  90. }
  91. if body.IndustryTag != "" {
  92. updates["industry_tag"] = body.IndustryTag
  93. }
  94. if body.Enabled != nil {
  95. updates["enabled"] = *body.Enabled
  96. }
  97. if err := h.store.DB.Model(&kw).Updates(updates).Error; err != nil {
  98. Fail(c, 500, err.Error())
  99. return
  100. }
  101. h.store.DB.First(&kw, id)
  102. LogAudit(h.store, c, "update", "keyword", strconv.FormatUint(id, 10), updates)
  103. OK(c, kw)
  104. }
  105. // Delete removes a keyword by ID.
  106. // DELETE /keywords/:id
  107. func (h *KeywordHandler) Delete(c *gin.Context) {
  108. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  109. if err != nil {
  110. Fail(c, http.StatusBadRequest, "invalid id")
  111. return
  112. }
  113. if err := h.store.DB.Delete(&model.Keyword{}, id).Error; err != nil {
  114. Fail(c, 500, err.Error())
  115. return
  116. }
  117. LogAudit(h.store, c, "delete", "keyword", strconv.FormatUint(id, 10), nil)
  118. OK(c, nil)
  119. }
  120. // ImportCSV handles POST /keywords/import — import keywords from CSV/TXT file
  121. func (h *KeywordHandler) ImportCSV(c *gin.Context) {
  122. file, header, err := c.Request.FormFile("file")
  123. if err != nil {
  124. Fail(c, 400, "请上传文件")
  125. return
  126. }
  127. defer file.Close()
  128. if header.Size > 5<<20 { // 5MB
  129. Fail(c, 400, "文件不能超过5MB")
  130. return
  131. }
  132. defaultTag := c.PostForm("industry_tag")
  133. // Read BOM
  134. bom := make([]byte, 3)
  135. n, _ := file.Read(bom)
  136. if n < 3 || bom[0] != 0xEF || bom[1] != 0xBB || bom[2] != 0xBF {
  137. file.Seek(0, io.SeekStart)
  138. }
  139. reader := csv.NewReader(file)
  140. reader.FieldsPerRecord = -1 // variable fields
  141. var imported, skipped int
  142. var errors []string
  143. rowNum := 0
  144. for {
  145. row, err := reader.Read()
  146. if err == io.EOF {
  147. break
  148. }
  149. rowNum++
  150. if err != nil {
  151. errors = append(errors, fmt.Sprintf("行 %d: 读取错误", rowNum))
  152. continue
  153. }
  154. // First column is keyword, optional second column is industry_tag
  155. if len(row) == 0 || strings.TrimSpace(row[0]) == "" {
  156. continue
  157. }
  158. keyword := strings.TrimSpace(row[0])
  159. // Skip header row
  160. if rowNum == 1 && (strings.EqualFold(keyword, "keyword") || keyword == "关键词") {
  161. continue
  162. }
  163. tag := defaultTag
  164. if len(row) > 1 && strings.TrimSpace(row[1]) != "" {
  165. tag = strings.TrimSpace(row[1])
  166. }
  167. kw := model.Keyword{
  168. Keyword: keyword,
  169. IndustryTag: tag,
  170. Enabled: true,
  171. }
  172. result := h.store.DB.Where(model.Keyword{Keyword: keyword}).FirstOrCreate(&kw)
  173. if result.RowsAffected > 0 {
  174. imported++
  175. } else {
  176. skipped++
  177. }
  178. }
  179. LogAudit(h.store, c, "import", "keyword", "", gin.H{"imported": imported, "skipped": skipped})
  180. OK(c, gin.H{
  181. "imported": imported,
  182. "skipped": skipped,
  183. "errors": errors,
  184. })
  185. }