mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-30 23:14:28 +08:00
文本审核记录功能完成
This commit is contained in:
@@ -53,4 +53,21 @@ var ModerationCategories = map[string]string{
|
|||||||
"porn": "明确的色情内容",
|
"porn": "明确的色情内容",
|
||||||
"insult": "具有侮辱、攻击性语言、人身攻击或冒犯性表达",
|
"insult": "具有侮辱、攻击性语言、人身攻击或冒犯性表达",
|
||||||
"violence": "包含暴力、血腥、攻击行为或煽动暴力的言论",
|
"violence": "包含暴力、血腥、攻击行为或煽动暴力的言论",
|
||||||
|
"illegal": "涉及违法活动的内容,如诈骗、赌博等",
|
||||||
|
"terror": "宣扬恐怖主义、极端暴力或煽动恐怖行为的内容",
|
||||||
|
"ad": "垃圾广告或未经许可的推广内容",
|
||||||
|
"spam": "无意义重复内容或诱导性信息",
|
||||||
|
"abuse": "人身攻击、恶意辱骂或侮辱性言论",
|
||||||
|
"polity": "涉及国家政治、领导人或政策的违规讨论内容",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 敏感词来源
|
||||||
|
const (
|
||||||
|
ModerationSourceChat = "chat"
|
||||||
|
ModerationSourceMJ = "mj"
|
||||||
|
ModerationSourceDalle = "dalle"
|
||||||
|
ModerationSourceSD = "sd"
|
||||||
|
ModerationSourceSuno = "suno"
|
||||||
|
ModerationSourceVideo = "video"
|
||||||
|
ModerationSourceJiMeng = "jimeng"
|
||||||
|
)
|
||||||
|
|||||||
231
api/handler/admin/moderation_handler.go
Normal file
231
api/handler/admin/moderation_handler.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||||||
|
// * Use of this source code is governed by a Apache-2.0 license
|
||||||
|
// * that can be found in the LICENSE file.
|
||||||
|
// * @Author yangjian102621@163.com
|
||||||
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
import (
|
||||||
|
"geekai/core"
|
||||||
|
"geekai/core/middleware"
|
||||||
|
"geekai/core/types"
|
||||||
|
"geekai/handler"
|
||||||
|
"geekai/store/model"
|
||||||
|
"geekai/utils"
|
||||||
|
"geekai/utils/resp"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModerationHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModerationHandler(app *core.AppServer, db *gorm.DB) *ModerationHandler {
|
||||||
|
return &ModerationHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes 注册路由
|
||||||
|
func (h *ModerationHandler) RegisterRoutes() {
|
||||||
|
group := h.App.Engine.Group("/api/admin/moderation/")
|
||||||
|
|
||||||
|
// 需要管理员授权的接口
|
||||||
|
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
||||||
|
{
|
||||||
|
group.POST("list", h.List)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
group.POST("batch-remove", h.BatchRemove)
|
||||||
|
group.GET("source-list", h.GetSourceList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取文本审核记录列表
|
||||||
|
func (h *ModerationHandler) List(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := h.DB.Session(&gorm.Session{})
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
if data.Username != "" {
|
||||||
|
// 通过用户名查找用户ID
|
||||||
|
var user model.User
|
||||||
|
if err := h.DB.Where("username LIKE ?", "%"+data.Username+"%").First(&user).Error; err == nil {
|
||||||
|
session = session.Where("user_id", user.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Source != "" {
|
||||||
|
session = session.Where("source", data.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.StartDate != "" && data.EndDate != "" {
|
||||||
|
startTime := data.StartDate + " 00:00:00"
|
||||||
|
endTime := data.EndDate + " 23:59:59"
|
||||||
|
session = session.Where("created_at >= ? AND created_at <= ?", startTime, endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计总数
|
||||||
|
var total int64
|
||||||
|
session.Model(&model.Moderation{}).Count(&total)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
page := data.Page
|
||||||
|
pageSize := data.PageSize
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
session = session.Offset(offset).Limit(pageSize)
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
var items []model.Moderation
|
||||||
|
err := session.Order("id DESC").Find(&items).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
userIds := make([]uint, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
userIds = append(userIds, item.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []model.User
|
||||||
|
if len(userIds) > 0 {
|
||||||
|
h.DB.Where("id IN ?", userIds).Find(&users)
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap := make(map[uint]string)
|
||||||
|
for _, user := range users {
|
||||||
|
userMap[user.Id] = user.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为响应数据
|
||||||
|
list := make([]map[string]any, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
var moderation types.ModerationResult
|
||||||
|
err := utils.JsonDecode(item.Result, &moderation)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var result []string
|
||||||
|
for value, label := range types.ModerationCategories {
|
||||||
|
if moderation.Categories[value] {
|
||||||
|
result = append(result, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list = append(list, map[string]any{
|
||||||
|
"id": item.Id,
|
||||||
|
"user_id": item.UserId,
|
||||||
|
"username": userMap[item.UserId],
|
||||||
|
"source": item.Source,
|
||||||
|
"input": item.Input,
|
||||||
|
"output": item.Output,
|
||||||
|
"result": result,
|
||||||
|
"created_at": item.CreatedAt.Unix(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, map[string]any{
|
||||||
|
"items": list,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ModerationHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
if id <= 0 {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.DB.Where("id", id).Delete(&model.Moderation{}).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchRemove 批量删除文本审核记录
|
||||||
|
func (h *ModerationHandler) BatchRemove(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Ids []uint `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.Ids) == 0 {
|
||||||
|
resp.ERROR(c, "请选择要删除的记录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.Moderation{}).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 source 列表
|
||||||
|
func (h *ModerationHandler) GetSourceList(c *gin.Context) {
|
||||||
|
sources := []gin.H{
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceChat,
|
||||||
|
"name": "AI对话",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceMJ,
|
||||||
|
"name": "Midjourney 绘图",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceDalle,
|
||||||
|
"name": "Dalle 绘图",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceSD,
|
||||||
|
"name": "StableDiffusion 绘图",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceSuno,
|
||||||
|
"name": "Suno 音乐",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceVideo,
|
||||||
|
"name": "视频生成",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": types.ModerationSourceJiMeng,
|
||||||
|
"name": "即梦AI",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, sources)
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/store/vo"
|
"geekai/store/vo"
|
||||||
@@ -62,21 +63,23 @@ type ChatInput struct {
|
|||||||
|
|
||||||
type ChatHandler struct {
|
type ChatHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
uploadManager *oss.UploaderManager
|
uploadManager *oss.UploaderManager
|
||||||
licenseService *service.LicenseService
|
licenseService *service.LicenseService
|
||||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler {
|
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService, moderationManager *moderation.ServiceManager) *ChatHandler {
|
||||||
return &ChatHandler{
|
return &ChatHandler{
|
||||||
BaseHandler: BaseHandler{App: app, DB: db},
|
BaseHandler: BaseHandler{App: app, DB: db},
|
||||||
redis: redis,
|
redis: redis,
|
||||||
uploadManager: manager,
|
uploadManager: manager,
|
||||||
licenseService: licenseService,
|
licenseService: licenseService,
|
||||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +312,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
|||||||
}
|
}
|
||||||
reqMgs := make([]any, 0)
|
reqMgs := make([]any, 0)
|
||||||
|
|
||||||
|
// 添加引导提示词,防止模型生成违规内容
|
||||||
|
if h.App.SysConfig.Moderation.EnableGuide {
|
||||||
|
reqMgs = append(reqMgs, map[string]any{
|
||||||
|
"role": "system",
|
||||||
|
"content": h.App.SysConfig.Moderation.GuidePrompt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for i := len(chatCtx) - 1; i >= 0; i-- {
|
for i := len(chatCtx) - 1; i >= 0; i-- {
|
||||||
reqMgs = append(reqMgs, chatCtx[i])
|
reqMgs = append(reqMgs, chatCtx[i])
|
||||||
}
|
}
|
||||||
@@ -352,16 +363,16 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(imgList) > 0 {
|
if len(imgList) > 0 {
|
||||||
imgList = append(imgList, map[string]interface{}{
|
imgList = append(imgList, map[string]any{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": input.Prompt,
|
"text": input.Prompt,
|
||||||
})
|
})
|
||||||
req.Messages = append(reqMgs, map[string]interface{}{
|
req.Messages = append(reqMgs, map[string]any{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": imgList,
|
"content": imgList,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
req.Messages = append(reqMgs, map[string]interface{}{
|
req.Messages = append(reqMgs, map[string]any{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": finalPrompt,
|
"content": finalPrompt,
|
||||||
})
|
})
|
||||||
@@ -557,6 +568,34 @@ func (h *ChatHandler) saveChatHistory(
|
|||||||
promptCreatedAt time.Time,
|
promptCreatedAt time.Time,
|
||||||
replyCreatedAt time.Time) {
|
replyCreatedAt time.Time) {
|
||||||
|
|
||||||
|
// 文本审核
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(usage.Content)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
logger.Debugf("moderationResult: %+v", moderationResult)
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
Source: types.ModerationSourceChat,
|
||||||
|
Input: usage.Prompt,
|
||||||
|
Output: usage.Content,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
pushMessage(c, ChatEventError, "很抱歉,内容触发敏感词预警,AI 无法回答!!!")
|
||||||
|
// 更新用户算力
|
||||||
|
if input.ChatModel.Power > 0 {
|
||||||
|
h.subUserPower(userVo, input, 0, 0)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
// 追加聊天记录
|
// 追加聊天记录
|
||||||
// for prompt
|
// for prompt
|
||||||
var promptTokens, replyTokens, totalTokens int
|
var promptTokens, replyTokens, totalTokens int
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
"geekai/service/dalle"
|
"geekai/service/dalle"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/store/vo"
|
"geekai/store/vo"
|
||||||
@@ -26,16 +27,18 @@ import (
|
|||||||
|
|
||||||
type DallJobHandler struct {
|
type DallJobHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
dallService *dalle.Service
|
dallService *dalle.Service
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler {
|
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *DallJobHandler {
|
||||||
return &DallJobHandler{
|
return &DallJobHandler{
|
||||||
dallService: service,
|
dallService: service,
|
||||||
uploader: manager,
|
uploader: manager,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
BaseHandler: BaseHandler{
|
BaseHandler: BaseHandler{
|
||||||
App: app,
|
App: app,
|
||||||
DB: db,
|
DB: db,
|
||||||
@@ -69,6 +72,29 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 文本审核
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: h.GetLoginUserId(c),
|
||||||
|
Source: types.ModerationSourceDalle,
|
||||||
|
Input: data.Prompt,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
resp.ERROR(c, "当前创作内容包含敏感词,提示词未通过文本审核,请重新输入!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var chatModel model.ChatModel
|
var chatModel model.ChatModel
|
||||||
if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
|
if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
|
||||||
resp.ERROR(c, "模型不存在")
|
resp.ERROR(c, "模型不存在")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
"geekai/service/jimeng"
|
"geekai/service/jimeng"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/store/vo"
|
"geekai/store/vo"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
@@ -19,16 +20,18 @@ import (
|
|||||||
// JimengHandler 即梦AI处理器
|
// JimengHandler 即梦AI处理器
|
||||||
type JimengHandler struct {
|
type JimengHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
jimengService *jimeng.Service
|
jimengService *jimeng.Service
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJimengHandler 创建即梦AI处理器
|
// NewJimengHandler 创建即梦AI处理器
|
||||||
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB, userService *service.UserService) *JimengHandler {
|
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB, userService *service.UserService, moderationManager *moderation.ServiceManager) *JimengHandler {
|
||||||
return &JimengHandler{
|
return &JimengHandler{
|
||||||
BaseHandler: BaseHandler{App: app, DB: db},
|
BaseHandler: BaseHandler{App: app, DB: db},
|
||||||
jimengService: jimengService,
|
jimengService: jimengService,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +78,31 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
|
|||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 文本审核
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: h.GetLoginUserId(c),
|
||||||
|
Source: types.ModerationSourceJiMeng,
|
||||||
|
Input: req.Prompt,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// 新增:除图像特效外,其他任务类型必须有提示词
|
// 新增:除图像特效外,其他任务类型必须有提示词
|
||||||
if req.TaskType != "image_effects" && req.Prompt == "" {
|
if req.TaskType != "image_effects" && req.Prompt == "" {
|
||||||
resp.ERROR(c, "提示词不能为空")
|
resp.ERROR(c, "提示词不能为空")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
"geekai/service/mj"
|
"geekai/service/mj"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
"geekai/store/vo"
|
"geekai/store/vo"
|
||||||
@@ -28,18 +29,20 @@ import (
|
|||||||
|
|
||||||
type MidJourneyHandler struct {
|
type MidJourneyHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
mjService *mj.Service
|
mjService *mj.Service
|
||||||
snowflake *service.Snowflake
|
snowflake *service.Snowflake
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler {
|
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *MidJourneyHandler {
|
||||||
return &MidJourneyHandler{
|
return &MidJourneyHandler{
|
||||||
snowflake: snowflake,
|
snowflake: snowflake,
|
||||||
mjService: service,
|
mjService: service,
|
||||||
uploader: manager,
|
uploader: manager,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
BaseHandler: BaseHandler{
|
BaseHandler: BaseHandler{
|
||||||
App: app,
|
App: app,
|
||||||
DB: db,
|
DB: db,
|
||||||
@@ -110,6 +113,29 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 文本审核
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: h.GetLoginUserId(c),
|
||||||
|
Source: types.ModerationSourceMJ,
|
||||||
|
Input: data.Prompt,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var params = ""
|
var params = ""
|
||||||
if data.Rate != "" && !strings.Contains(params, "--ar") {
|
if data.Rate != "" && !strings.Contains(params, "--ar") {
|
||||||
params += " --ar " + data.Rate
|
params += " --ar " + data.Rate
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/service/sd"
|
"geekai/service/sd"
|
||||||
"geekai/store"
|
"geekai/store"
|
||||||
@@ -29,12 +30,13 @@ import (
|
|||||||
|
|
||||||
type SdJobHandler struct {
|
type SdJobHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
sdService *sd.Service
|
sdService *sd.Service
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
snowflake *service.Snowflake
|
snowflake *service.Snowflake
|
||||||
leveldb *store.LevelDB
|
leveldb *store.LevelDB
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSdJobHandler(app *core.AppServer,
|
func NewSdJobHandler(app *core.AppServer,
|
||||||
@@ -43,13 +45,15 @@ func NewSdJobHandler(app *core.AppServer,
|
|||||||
manager *oss.UploaderManager,
|
manager *oss.UploaderManager,
|
||||||
snowflake *service.Snowflake,
|
snowflake *service.Snowflake,
|
||||||
userService *service.UserService,
|
userService *service.UserService,
|
||||||
levelDB *store.LevelDB) *SdJobHandler {
|
levelDB *store.LevelDB,
|
||||||
|
moderationManager *moderation.ServiceManager) *SdJobHandler {
|
||||||
return &SdJobHandler{
|
return &SdJobHandler{
|
||||||
sdService: service,
|
sdService: service,
|
||||||
uploader: manager,
|
uploader: manager,
|
||||||
snowflake: snowflake,
|
snowflake: snowflake,
|
||||||
leveldb: levelDB,
|
leveldb: levelDB,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
BaseHandler: BaseHandler{
|
BaseHandler: BaseHandler{
|
||||||
App: app,
|
App: app,
|
||||||
DB: db,
|
DB: db,
|
||||||
@@ -102,6 +106,29 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: h.GetLoginUserId(c),
|
||||||
|
Source: types.ModerationSourceSD,
|
||||||
|
Input: data.Prompt,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if data.Width <= 0 {
|
if data.Width <= 0 {
|
||||||
data.Width = 512
|
data.Width = 512
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/service/suno"
|
"geekai/service/suno"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
@@ -27,20 +28,22 @@ import (
|
|||||||
|
|
||||||
type SunoHandler struct {
|
type SunoHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
sunoService *suno.Service
|
sunoService *suno.Service
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
|
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *SunoHandler {
|
||||||
return &SunoHandler{
|
return &SunoHandler{
|
||||||
BaseHandler: BaseHandler{
|
BaseHandler: BaseHandler{
|
||||||
App: app,
|
App: app,
|
||||||
DB: db,
|
DB: db,
|
||||||
},
|
},
|
||||||
sunoService: service,
|
sunoService: service,
|
||||||
uploader: uploader,
|
uploader: uploader,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +87,29 @@ func (h *SunoHandler) Create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: h.GetLoginUserId(c),
|
||||||
|
Source: types.ModerationSourceSuno,
|
||||||
|
Input: data.Prompt,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
user, err := h.GetLoginUser(c)
|
user, err := h.GetLoginUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"geekai/core/middleware"
|
"geekai/core/middleware"
|
||||||
"geekai/core/types"
|
"geekai/core/types"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
|
"geekai/service/moderation"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/service/video"
|
"geekai/service/video"
|
||||||
"geekai/store/model"
|
"geekai/store/model"
|
||||||
@@ -27,20 +28,22 @@ import (
|
|||||||
|
|
||||||
type VideoHandler struct {
|
type VideoHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
videoService *video.Service
|
videoService *video.Service
|
||||||
uploader *oss.UploaderManager
|
uploader *oss.UploaderManager
|
||||||
userService *service.UserService
|
userService *service.UserService
|
||||||
|
moderationManager *moderation.ServiceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
|
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *VideoHandler {
|
||||||
return &VideoHandler{
|
return &VideoHandler{
|
||||||
BaseHandler: BaseHandler{
|
BaseHandler: BaseHandler{
|
||||||
App: app,
|
App: app,
|
||||||
DB: db,
|
DB: db,
|
||||||
},
|
},
|
||||||
videoService: service,
|
videoService: service,
|
||||||
uploader: uploader,
|
uploader: uploader,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
moderationManager: moderationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +81,29 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.App.SysConfig.Moderation.Enable {
|
||||||
|
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to moderate content: ", err)
|
||||||
|
}
|
||||||
|
if moderationResult.Flagged {
|
||||||
|
// 记录违规内容
|
||||||
|
moderation := model.Moderation{
|
||||||
|
UserId: h.GetLoginUserId(c),
|
||||||
|
Source: types.ModerationSourceVideo,
|
||||||
|
Input: data.Prompt,
|
||||||
|
Result: utils.JsonEncode(moderationResult),
|
||||||
|
}
|
||||||
|
err = h.DB.Create(&moderation).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to save moderation: ", err)
|
||||||
|
}
|
||||||
|
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
user, err := h.GetLoginUser(c)
|
user, err := h.GetLoginUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
|
|||||||
@@ -247,6 +247,10 @@ func main() {
|
|||||||
fx.Provide(moderation.NewBaiduAIModeration),
|
fx.Provide(moderation.NewBaiduAIModeration),
|
||||||
fx.Provide(moderation.NewTencentAIModeration),
|
fx.Provide(moderation.NewTencentAIModeration),
|
||||||
fx.Provide(moderation.NewServiceManager),
|
fx.Provide(moderation.NewServiceManager),
|
||||||
|
fx.Provide(admin.NewModerationHandler),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.ModerationHandler) {
|
||||||
|
h.RegisterRoutes()
|
||||||
|
}),
|
||||||
|
|
||||||
// 注册路由
|
// 注册路由
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
|
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import "time"
|
|||||||
type Moderation struct {
|
type Moderation struct {
|
||||||
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"`
|
UserId uint `gorm:"column:user_id;type:int(11);not null;comment:用户ID" json:"user_id"`
|
||||||
Input string `gorm:"column:prompt;type:text;not null;comment:用户输入" json:"input"`
|
Source string `gorm:"column:source;type:varchar(255);not null;comment:敏感词来源" json:"source"`
|
||||||
|
Input string `gorm:"column:input;type:text;not null;comment:用户输入" json:"input"`
|
||||||
Output string `gorm:"column:output;type:text;not null;comment:AI 输出" json:"output"`
|
Output string `gorm:"column:output;type:text;not null;comment:AI 输出" json:"output"`
|
||||||
Result string `gorm:"column:result;type:text;not null;comment:鉴别结果" json:"result"`
|
Result string `gorm:"column:result;type:text;not null;comment:鉴别结果" json:"result"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null" json:"created_at"`
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.el-dialog__header {
|
.el-dialog__header {
|
||||||
.el-dialog__title {
|
.el-dialog__title {
|
||||||
color: #f5f5f5;
|
color: var(--text-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,4 +91,4 @@
|
|||||||
// end el-row
|
// end el-row
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ const items = [
|
|||||||
index: '/admin/config/moderation',
|
index: '/admin/config/moderation',
|
||||||
title: '审查配置',
|
title: '审查配置',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'list',
|
||||||
|
index: '/admin/moderation/list',
|
||||||
|
title: '审核记录',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -213,7 +213,13 @@ const routes = [
|
|||||||
path: '/admin/config/moderation',
|
path: '/admin/config/moderation',
|
||||||
name: 'admin-config-moderation',
|
name: 'admin-config-moderation',
|
||||||
meta: { title: '文本审查配置' },
|
meta: { title: '文本审查配置' },
|
||||||
component: () => import('@/views/admin/settings/ModerationConfig.vue'),
|
component: () => import('@/views/admin/moderation/ModerationConfig.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/moderation/list',
|
||||||
|
name: 'admin-moderation-list',
|
||||||
|
meta: { title: '文本审核记录' },
|
||||||
|
component: () => import('@/views/admin/moderation/ModerationList.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/config/markmap',
|
path: '/admin/config/markmap',
|
||||||
|
|||||||
499
web/src/views/admin/moderation/ModerationList.vue
Normal file
499
web/src/views/admin/moderation/ModerationList.vue
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
<template>
|
||||||
|
<div class="moderation-list container p-5">
|
||||||
|
<!-- 搜索筛选区域 -->
|
||||||
|
<el-card shadow="never" class="mb-6">
|
||||||
|
<el-form :model="queryForm" label-position="top" class="search-form">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="来源">
|
||||||
|
<el-select
|
||||||
|
v-model="queryForm.source"
|
||||||
|
placeholder="请选择来源"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in sourceList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="时间范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryForm.dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mt-4">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<el-button type="primary" @click="handleSearch" :loading="loading">
|
||||||
|
<i class="iconfont icon-search mr-1"></i>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<i class="iconfont icon-refresh mr-1"></i>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
共找到 <span class="font-semibold text-blue-600">{{ total }}</span> 条记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 数据列表 -->
|
||||||
|
<el-card shadow="never">
|
||||||
|
<div class="table-header flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700">审核记录列表</h3>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleBatchDelete"
|
||||||
|
:disabled="selectedRows.length === 0"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-delete mr-1"></i>
|
||||||
|
批量删除 ({{ selectedRows.length }})
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="tableData"
|
||||||
|
v-loading="loading"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
|
||||||
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="username" label="用户名" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-medium text-gray-700">{{ row.username || '未知用户' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="source" label="来源" width="140" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="primary" size="small">
|
||||||
|
{{ getSourceLabel(row.source) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="input" label="用户输入" max-width="300">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="text-content">
|
||||||
|
<span class="text-display">
|
||||||
|
{{
|
||||||
|
row.input
|
||||||
|
? row.input.length > 30
|
||||||
|
? row.input.substring(0, 30) + '...'
|
||||||
|
: row.input
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="output" label="AI 输出" max-width="300">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="text-content">
|
||||||
|
<span class="text-display">
|
||||||
|
{{
|
||||||
|
row.output
|
||||||
|
? row.output.length > 30
|
||||||
|
? row.output.substring(0, 30) + '...'
|
||||||
|
: row.output
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="180" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="text-gray-600">{{ dateFormat(row.created_at) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<el-button type="primary" size="small" @click="handleView(row)">
|
||||||
|
<i class="iconfont icon-view mr-1"></i>
|
||||||
|
查看
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(row)">
|
||||||
|
<i class="iconfont icon-delete mr-1"></i>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper flex justify-center mt-6">
|
||||||
|
<el-pagination
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:page-sizes="[15, 30, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 查看详情弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="审核记录详情"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div v-if="currentRecord" class="record-detail">
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<div class="detail-item">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">用户信息</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded">
|
||||||
|
<span class="text-gray-900">{{ currentRecord.username || '未知用户' }}</span>
|
||||||
|
<span class="text-gray-500 ml-4">ID: {{ currentRecord.user_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">来源</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded">
|
||||||
|
<el-tag type="primary" size="small">
|
||||||
|
{{ getSourceLabel(currentRecord.source) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">用户输入</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded">
|
||||||
|
<pre class="whitespace-pre-wrap text-sm text-gray-900">{{ currentRecord.input }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">AI 输出</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded">
|
||||||
|
<pre class="whitespace-pre-wrap text-sm text-gray-900">{{
|
||||||
|
currentRecord.output
|
||||||
|
}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">审核结果</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded">
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<el-tag
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
v-for="item in currentRecord.result"
|
||||||
|
:key="item"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-item">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">创建时间</label>
|
||||||
|
<div class="bg-gray-50 p-3 rounded">
|
||||||
|
<span class="text-gray-900">{{ dateFormat(currentRecord.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="danger" @click="handleDelete(currentRecord)"> 删除记录 </el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { httpGet, httpPost } from '@/utils/http'
|
||||||
|
import { dateFormat } from '@/utils/libs'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const selectedRows = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(15)
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentRecord = ref(null)
|
||||||
|
const sourceList = ref([])
|
||||||
|
|
||||||
|
// 查询表单
|
||||||
|
const queryForm = ref({
|
||||||
|
username: '',
|
||||||
|
source: '',
|
||||||
|
dateRange: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const hasFilters = computed(() => {
|
||||||
|
return queryForm.value.username || queryForm.value.source || queryForm.value.dateRange.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
fetchSourceList()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取来源列表
|
||||||
|
const fetchSourceList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await httpGet('/api/admin/moderation/source-list')
|
||||||
|
sourceList.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取来源列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
...queryForm.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理日期范围
|
||||||
|
if (params.dateRange && params.dateRange.length === 2) {
|
||||||
|
params.start_date = params.dateRange[0]
|
||||||
|
params.end_date = params.dateRange[1]
|
||||||
|
delete params.dateRange
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await httpPost('/api/admin/moderation/list', params)
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
tableData.value = response.data.items || []
|
||||||
|
total.value = response.data.total || 0
|
||||||
|
currentPage.value = response.data.page || 1
|
||||||
|
pageSize.value = response.data.page_size || 15
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取数据失败:' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
queryForm.value = {
|
||||||
|
username: '',
|
||||||
|
source: '',
|
||||||
|
dateRange: [],
|
||||||
|
}
|
||||||
|
currentPage.value = 1
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pageSize.value = size
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
currentPage.value = page
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择处理
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
selectedRows.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (row) => {
|
||||||
|
currentRecord.value = row
|
||||||
|
detailDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除单条记录
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这条审核记录吗?删除后无法恢复。', '确认删除', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
|
||||||
|
await httpGet(`/api/admin/moderation/remove?id=${row.id}`)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
// 如果当前查看的记录被删除,关闭弹窗
|
||||||
|
if (detailDialogVisible.value && currentRecord.value?.id === row.id) {
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
currentRecord.value = null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败:' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (selectedRows.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要删除的记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除选中的 ${selectedRows.value.length} 条审核记录吗?删除后无法恢复。`,
|
||||||
|
'确认批量删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const ids = selectedRows.value.map((row) => row.id)
|
||||||
|
await httpPost('/api/admin/moderation/batch-remove', { ids })
|
||||||
|
|
||||||
|
ElMessage.success('批量删除成功')
|
||||||
|
selectedRows.value = []
|
||||||
|
fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('批量删除失败:' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSourceLabel = (source) => {
|
||||||
|
const sourceMap = sourceList.value.find((item) => item.id === source)
|
||||||
|
return sourceMap.name || source || '未知'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.moderation-list {
|
||||||
|
.page-header {
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
.el-select__wrapper {
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
max-width: 300px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-detail {
|
||||||
|
.detail-item {
|
||||||
|
label {
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-50 {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.moderation-list {
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user