| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- package handler
- import (
- "encoding/csv"
- "fmt"
- "io"
- "net/http"
- "strconv"
- "strings"
- "spider/internal/model"
- "spider/internal/store"
- "github.com/gin-gonic/gin"
- )
- // KeywordHandler handles unified keyword + seed CRUD.
- type KeywordHandler struct {
- store *store.Store
- }
- // List returns keywords with optional filters and pagination.
- // GET /keywords?page=1&page_size=20&industry_tag=
- func (h *KeywordHandler) List(c *gin.Context) {
- page, pageSize, offset := parsePage(c)
- industryTag := c.Query("industry_tag")
- query := h.store.DB.Model(&model.Keyword{})
- if industryTag != "" {
- query = query.Where("industry_tag = ?", industryTag)
- }
- var total int64
- query.Count(&total)
- var keywords []model.Keyword
- if err := query.Order("id DESC").Limit(pageSize).Offset(offset).Find(&keywords).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- PageOK(c, keywords, total, page, pageSize)
- }
- // Create creates one or more keywords in batch.
- // POST /keywords body: {keywords:["k1","k2"], industry_tag:"机场"}
- func (h *KeywordHandler) Create(c *gin.Context) {
- var body struct {
- Keywords []string `json:"keywords" binding:"required,min=1"`
- IndustryTag string `json:"industry_tag"`
- }
- if err := c.ShouldBindJSON(&body); err != nil {
- Fail(c, http.StatusBadRequest, err.Error())
- return
- }
- var created []model.Keyword
- for _, kw := range body.Keywords {
- if kw == "" {
- continue
- }
- k := model.Keyword{
- Keyword: kw,
- IndustryTag: body.IndustryTag,
- Enabled: true,
- }
- if err := h.store.DB.Where(model.Keyword{Keyword: kw}).FirstOrCreate(&k).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- created = append(created, k)
- }
- LogAudit(h.store, c, "create", "keyword", "", gin.H{"count": len(created)})
- OK(c, created)
- }
- // Update modifies a keyword.
- // PUT /keywords/:id
- func (h *KeywordHandler) Update(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 64)
- if err != nil {
- Fail(c, http.StatusBadRequest, "invalid id")
- return
- }
- var body struct {
- Keyword string `json:"keyword"`
- IndustryTag string `json:"industry_tag"`
- Enabled *bool `json:"enabled"`
- }
- if err := c.ShouldBindJSON(&body); err != nil {
- Fail(c, http.StatusBadRequest, err.Error())
- return
- }
- var kw model.Keyword
- if err := h.store.DB.First(&kw, id).Error; err != nil {
- Fail(c, 404, "keyword not found")
- return
- }
- updates := map[string]any{}
- if body.Keyword != "" {
- updates["keyword"] = body.Keyword
- }
- if body.IndustryTag != "" {
- updates["industry_tag"] = body.IndustryTag
- }
- if body.Enabled != nil {
- updates["enabled"] = *body.Enabled
- }
- if err := h.store.DB.Model(&kw).Updates(updates).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- h.store.DB.First(&kw, id)
- LogAudit(h.store, c, "update", "keyword", strconv.FormatUint(id, 10), updates)
- OK(c, kw)
- }
- // Delete removes a keyword by ID.
- // DELETE /keywords/:id
- func (h *KeywordHandler) Delete(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 64)
- if err != nil {
- Fail(c, http.StatusBadRequest, "invalid id")
- return
- }
- if err := h.store.DB.Delete(&model.Keyword{}, id).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- LogAudit(h.store, c, "delete", "keyword", strconv.FormatUint(id, 10), nil)
- OK(c, nil)
- }
- // ImportCSV handles POST /keywords/import — import keywords from CSV/TXT file
- func (h *KeywordHandler) ImportCSV(c *gin.Context) {
- file, header, err := c.Request.FormFile("file")
- if err != nil {
- Fail(c, 400, "请上传文件")
- return
- }
- defer file.Close()
- if header.Size > 5<<20 { // 5MB
- Fail(c, 400, "文件不能超过5MB")
- return
- }
- defaultTag := c.PostForm("industry_tag")
- // Read BOM
- bom := make([]byte, 3)
- n, _ := file.Read(bom)
- if n < 3 || bom[0] != 0xEF || bom[1] != 0xBB || bom[2] != 0xBF {
- file.Seek(0, io.SeekStart)
- }
- reader := csv.NewReader(file)
- reader.FieldsPerRecord = -1 // variable fields
- var imported, skipped int
- var errors []string
- rowNum := 0
- for {
- row, err := reader.Read()
- if err == io.EOF {
- break
- }
- rowNum++
- if err != nil {
- errors = append(errors, fmt.Sprintf("行 %d: 读取错误", rowNum))
- continue
- }
- // First column is keyword, optional second column is industry_tag
- if len(row) == 0 || strings.TrimSpace(row[0]) == "" {
- continue
- }
- keyword := strings.TrimSpace(row[0])
- // Skip header row
- if rowNum == 1 && (strings.EqualFold(keyword, "keyword") || keyword == "关键词") {
- continue
- }
- tag := defaultTag
- if len(row) > 1 && strings.TrimSpace(row[1]) != "" {
- tag = strings.TrimSpace(row[1])
- }
- kw := model.Keyword{
- Keyword: keyword,
- IndustryTag: tag,
- Enabled: true,
- }
- result := h.store.DB.Where(model.Keyword{Keyword: keyword}).FirstOrCreate(&kw)
- if result.RowsAffected > 0 {
- imported++
- } else {
- skipped++
- }
- }
- LogAudit(h.store, c, "import", "keyword", "", gin.H{"imported": imported, "skipped": skipped})
- OK(c, gin.H{
- "imported": imported,
- "skipped": skipped,
- "errors": errors,
- })
- }
|