增加违禁词管理 增加多选操作

This commit is contained in:
2026-07-05 03:00:49 +08:00
parent 4d5b49c2a9
commit 10d0732f8c
24 changed files with 836 additions and 35 deletions
+12 -1
View File
@@ -29,6 +29,7 @@ func (h *AdminReadHandler) Users(c *gin.Context) {
for _, user := range users {
row := userPublic(user)
row["generation_count"] = user.GenerationCount
row["banned_word_hits"] = user.BannedWordHits
out = append(out, row)
}
c.JSON(http.StatusOK, gin.H{"data": out, "stats": stats})
@@ -56,7 +57,7 @@ func (h *AdminReadHandler) Logs(c *gin.Context) {
}
}
items, total, stats, err := h.admin.Logs(c.Request.Context(), limit, offset, kind, status, nil, since, "", "", c.Query("source"), false)
items, total, stats, err := h.admin.Logs(c.Request.Context(), limit, offset, kind, status, nil, since, "", "", c.Query("source"), false, false, false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
return
@@ -158,6 +159,16 @@ func (h *AdminReadHandler) Invites(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": items, "stats": stats})
}
// DeleteImage removes one generated file (plus derived stills) and blanks the
// log rows referencing it. Admin 图片管理 delete; ?name= is the storage key.
func (h *AdminReadHandler) DeleteImage(c *gin.Context) {
if err := h.admin.DeleteFile(c.Request.Context(), c.Query("name")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *AdminReadHandler) Providers(c *gin.Context) {
items, err := h.admin.Providers(c.Request.Context())
if err != nil {
@@ -0,0 +1,65 @@
package handler
import (
"net/http"
"backend/internal/repo"
"github.com/gin-gonic/gin"
)
// BannedWordsHandler — admin 违禁词管理: list / add / delete prompt blocklist
// entries. The generation path (V1Service.checkBannedPrompt) enforces them.
type BannedWordsHandler struct {
words *repo.BannedWordRepository
}
func NewBannedWordsHandler(words *repo.BannedWordRepository) *BannedWordsHandler {
return &BannedWordsHandler{words: words}
}
func (h *BannedWordsHandler) List(c *gin.Context) {
items, err := h.words.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load banned words"})
return
}
out := make([]gin.H, 0, len(items))
for _, w := range items {
out = append(out, gin.H{
"id": w.ID,
"word": w.Word,
"hits": w.Hits,
"created_at": w.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"data": out})
}
func (h *BannedWordsHandler) Create(c *gin.Context) {
var body struct {
Word string `json:"word"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
return
}
item, err := h.words.Create(c.Request.Context(), body.Word)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{"id": item.ID, "word": item.Word, "hits": item.Hits, "created_at": item.CreatedAt}})
}
func (h *BannedWordsHandler) Delete(c *gin.Context) {
n, err := h.words.Delete(c.Request.Context(), c.Param("id"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "delete failed"})
return
}
if n == 0 {
c.JSON(http.StatusNotFound, gin.H{"detail": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
@@ -38,6 +38,22 @@ func (h *UserGenerationHandler) MyImages(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": items})
}
// DeleteMyFile removes ONE of the caller's own generated files (plus its
// thumbnail) and blanks the log rows referencing it, so the 画图台 grid and
// 创作记录 gallery stop showing it. ?file= is the storage key (owner/name).
func (h *UserGenerationHandler) DeleteMyFile(c *gin.Context) {
user := currentUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, gin.H{"detail": "未登录或会话已过期"})
return
}
if err := h.admin.DeleteOwnedFile(c.Request.Context(), service.OwnerDir(user), c.Query("file")); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *UserGenerationHandler) Generate(c *gin.Context) {
user := currentUser(c)
if user == nil {
@@ -70,7 +86,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
switch {
case errors.Is(err, service.ErrUnknownModel):
c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrUnsupportedParams):
case errors.Is(err, service.ErrUnsupportedParams), errors.Is(err, service.ErrBannedPrompt):
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrInsufficientFunds):
c.JSON(http.StatusPaymentRequired, gin.H{"detail": "积分不足"})
@@ -132,7 +148,7 @@ func (h *UserGenerationHandler) Test(c *gin.Context) {
switch {
case errors.Is(err, service.ErrUnknownModel):
c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrUnsupportedParams):
case errors.Is(err, service.ErrUnsupportedParams), errors.Is(err, service.ErrBannedPrompt):
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrProviderQuota):
c.JSON(http.StatusTooManyRequests, gin.H{"detail": err.Error()})
@@ -206,7 +222,14 @@ func (h *UserGenerationHandler) Logs(c *gin.Context) {
// rows with real media (success + stored file), not failed/pending events.
hasFile := c.Query("has_file") == "1" || c.Query("has_file") == "true"
items, total, stats, err := h.admin.Logs(c.Request.Context(), limit, offset, kind, status, statuses, nil, userID, excludeSource, source, hasFile)
// Media views hide homepage showcase files — those belong to the public
// landing page, not to the caller's personal works. Galleries imply it via
// has_file; the 画图台 grid opts in with exclude_showcase=1.
excludeShowcase := hasFile || c.Query("exclude_showcase") == "1"
// media=1 (画图台 grid): only pending rows or rows with a stored file, so a
// deleted work's blanked row doesn't consume one of the grid's slots.
mediaOnly := c.Query("media") == "1"
items, total, stats, err := h.admin.Logs(c.Request.Context(), limit, offset, kind, status, statuses, nil, userID, excludeSource, source, hasFile, excludeShowcase, mediaOnly)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load logs"})
return
+1 -1
View File
@@ -316,7 +316,7 @@ func (h *V1Handler) writeV1Error(c *gin.Context, err error, payload map[string]a
switch {
case errors.Is(err, service.ErrUnknownModel):
c.JSON(http.StatusNotFound, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrUnsupportedParams):
case errors.Is(err, service.ErrUnsupportedParams), errors.Is(err, service.ErrBannedPrompt):
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
case errors.Is(err, service.ErrInsufficientFunds):
c.JSON(http.StatusPaymentRequired, gin.H{"detail": err.Error()})
+6
View File
@@ -28,6 +28,7 @@ type Handlers struct {
ConcGroups *handler.ConcurrencyGroupHandler
Announcement *handler.AnnouncementHandler
Payment *handler.PaymentHandler
BannedWords *handler.BannedWordsHandler
}
func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.Engine {
@@ -87,6 +88,7 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
userAuthed.POST("/test", handlers.UserGen.Test)
userAuthed.GET("/jobs/mine", handlers.UserGen.MyJobs)
userAuthed.GET("/my-images", handlers.UserGen.MyImages)
userAuthed.DELETE("/my-files", handlers.UserGen.DeleteMyFile)
userAuthed.GET("/announcement", handlers.Announcement.Get)
userAuthed.POST("/announcement/seen", handlers.Announcement.MarkSeen)
userAuthed.GET("/pay/config", handlers.Payment.Config)
@@ -137,6 +139,10 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
authed.GET("/accounts/:pool/:id/email", handlers.ProviderAdmin.AccountEmail)
authed.GET("/providers", handlers.AdminRead.Providers)
authed.GET("/images", handlers.AdminRead.Images)
authed.DELETE("/images", handlers.AdminRead.DeleteImage)
authed.GET("/banned-words", handlers.BannedWords.List)
authed.POST("/banned-words", handlers.BannedWords.Create)
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)
authed.PATCH("/refresh/profiles/:profile_id", handlers.ProviderAdmin.RefreshUpdate)