diff --git a/backend/internal/http/handler/banned_words.go b/backend/internal/http/handler/banned_words.go index 30356b7..b28b769 100644 --- a/backend/internal/http/handler/banned_words.go +++ b/backend/internal/http/handler/banned_words.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "strings" "backend/internal/repo" "github.com/gin-gonic/gin" @@ -51,6 +52,31 @@ func (h *BannedWordsHandler) Create(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": gin.H{"id": item.ID, "word": item.Word, "hits": item.Hits, "created_at": item.CreatedAt}}) } +// Import bulk-adds words from a free-form text blob — split on newlines, +// commas (英文/中文), 顿号 and semicolons — skipping blanks and existing entries. +func (h *BannedWordsHandler) Import(c *gin.Context) { + var body struct { + Text string `json:"text"` + } + if err := c.ShouldBindJSON(&body); err != nil || strings.TrimSpace(body.Text) == "" { + c.JSON(http.StatusBadRequest, gin.H{"detail": "请提供要导入的违禁词"}) + return + } + words := strings.FieldsFunc(body.Text, func(r rune) bool { + switch r { + case '\n', '\r', ',', ',', '、', ';', ';': + return true + } + return false + }) + added, skipped, err := h.words.BulkCreate(c.Request.Context(), words) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"detail": "导入失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"added": added, "skipped": skipped}) +} + func (h *BannedWordsHandler) Delete(c *gin.Context) { n, err := h.words.Delete(c.Request.Context(), c.Param("id")) if err != nil { diff --git a/backend/internal/http/router/router.go b/backend/internal/http/router/router.go index 792341f..9b489d8 100644 --- a/backend/internal/http/router/router.go +++ b/backend/internal/http/router/router.go @@ -142,6 +142,7 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin. authed.DELETE("/images", handlers.AdminRead.DeleteImage) authed.GET("/banned-words", handlers.BannedWords.List) authed.POST("/banned-words", handlers.BannedWords.Create) + authed.POST("/banned-words/import", handlers.BannedWords.Import) authed.DELETE("/banned-words/:id", handlers.BannedWords.Delete) authed.GET("/refresh/profiles", handlers.ProviderAdmin.RefreshProfiles) authed.POST("/refresh/profiles/:profile_id/refresh-now", handlers.ProviderAdmin.RefreshNow) diff --git a/backend/internal/repo/banned_word_repo.go b/backend/internal/repo/banned_word_repo.go index 7a2b113..3b30e83 100644 --- a/backend/internal/repo/banned_word_repo.go +++ b/backend/internal/repo/banned_word_repo.go @@ -21,7 +21,7 @@ func NewBannedWordRepository(db *gorm.DB) *BannedWordRepository { func (r *BannedWordRepository) List(ctx context.Context) ([]model.BannedWord, error) { var items []model.BannedWord - err := r.db.WithContext(ctx).Order("hits DESC, created_at DESC").Find(&items).Error + err := r.db.WithContext(ctx).Order("created_at DESC, hits DESC").Find(&items).Error return items, err } @@ -42,6 +42,43 @@ func (r *BannedWordRepository) Create(ctx context.Context, word string) (*model. return item, nil } +// BulkCreate inserts the given words, skipping blanks and ones already in the +// table (case-insensitive). Returns how many were added vs skipped. +func (r *BannedWordRepository) BulkCreate(ctx context.Context, words []string) (added, skipped int, err error) { + existing, err := r.List(ctx) + if err != nil { + return 0, 0, err + } + seen := make(map[string]bool, len(existing)) + for _, w := range existing { + seen[strings.ToLower(w.Word)] = true + } + for _, w := range words { + w = strings.TrimSpace(w) + if w == "" { + continue + } + key := strings.ToLower(w) + if seen[key] { + skipped++ + continue + } + item := &model.BannedWord{ + ID: strings.ReplaceAll(uuid.NewString(), "-", "")[:32], + Word: w, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if e := r.db.WithContext(ctx).Create(item).Error; e != nil { + skipped++ + continue + } + seen[key] = true + added++ + } + return added, skipped, nil +} + func (r *BannedWordRepository) Delete(ctx context.Context, id string) (int64, error) { res := r.db.WithContext(ctx).Delete(&model.BannedWord{}, "id = ?", id) return res.RowsAffected, res.Error diff --git a/frontend/src/views/BannedWordsView.vue b/frontend/src/views/BannedWordsView.vue index f087ca0..6ec39c7 100644 --- a/frontend/src/views/BannedWordsView.vue +++ b/frontend/src/views/BannedWordsView.vue @@ -25,6 +25,24 @@ async function add() { else flash(r.data?.detail || '添加失败') } +// bulk import — paste words separated by newlines / commas / 、 / ; +const importOpen = ref(false) +const importText = ref('') +const importing = ref(false) +async function doImport() { + const text = importText.value.trim() + if (!text) { flash('请先粘贴要导入的违禁词'); return } + importing.value = true + const r = await api('/banned-words/import', jsonBody('POST', { text })) + importing.value = false + if (r.ok) { + importOpen.value = false + importText.value = '' + flash(`导入完成:新增 ${r.data?.added ?? 0} 个,跳过 ${r.data?.skipped ?? 0} 个`) + load() + } else flash(r.data?.detail || '导入失败') +} + async function del(w) { if (!confirm(`删除违禁词「${w.word}」?`)) return const r = await api(`/banned-words/${w.id}`, { method: 'DELETE' }) @@ -104,6 +122,7 @@ onMounted(load) + @@ -147,6 +166,18 @@ onMounted(load) +
每行一个,或用逗号、顿号、分号分隔;已存在的词会自动跳过。
+ +