@@ -0,0 +1,373 @@
|
||||
package sensitivewordctrl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/sensitivewordmod"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tealeg/xlsx"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// 导入/导出模版列表头(顺序即列序)
|
||||
var templateHeaders = []string{"一级分类", "词条"}
|
||||
|
||||
// List doc
|
||||
// @Summary 敏感词列表
|
||||
// @Description 分页查询敏感词,支持按分类、风险等级、关键词、状态筛选
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码,默认1"
|
||||
// @Param size query int false "每页条数,默认20"
|
||||
// @Param category query string false "一级分类"
|
||||
// @Param keyword query string false "词条模糊搜索"
|
||||
// @Param status query int false "状态 1-启用 0-禁用"
|
||||
// @Success 200 {string} json "{"msg":"操作成功","data":{"list":[],"total":0}}"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/list [get]
|
||||
func List(ctx *gin.Context) {
|
||||
var req sensitivewordmod.ListReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
list, total, err := sensitivewordmod.List(&req)
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrDbQueryError, nil)
|
||||
return
|
||||
}
|
||||
common.ServeJSON(ctx, stderr.Success, gin.H{
|
||||
"list": list,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// Add doc
|
||||
// @Summary 新增敏感词
|
||||
// @Description 新增单条敏感词
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body sensitivewordmod.AddReq true "敏感词信息"
|
||||
// @Success 200 {string} json "{"msg":"操作成功"}"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/add [post]
|
||||
func Add(ctx *gin.Context) {
|
||||
var req sensitivewordmod.AddReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
w := &sensitivewordmod.SensitiveWord{
|
||||
Category: req.Category,
|
||||
Word: req.Word,
|
||||
}
|
||||
if err := sensitivewordmod.Add(w); err != nil {
|
||||
common.ServeJSON(ctx, stderr.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
common.ServeJSON(ctx, stderr.Success, nil)
|
||||
}
|
||||
|
||||
// Update doc
|
||||
// @Summary 编辑敏感词
|
||||
// @Description 编辑单条敏感词,支持部分字段更新
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body sensitivewordmod.UpdateReq true "更新信息,id必传"
|
||||
// @Success 200 {string} json "{"msg":"操作成功"}"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/update [post]
|
||||
func Update(ctx *gin.Context) {
|
||||
var req sensitivewordmod.UpdateReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := primitive.ObjectIDFromHex(req.ID)
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, "invalid id")
|
||||
return
|
||||
}
|
||||
fields := bson.M{}
|
||||
if req.Category != "" {
|
||||
fields["category"] = req.Category
|
||||
}
|
||||
if req.Word != "" {
|
||||
fields["word"] = req.Word
|
||||
}
|
||||
if req.Status != nil {
|
||||
fields["status"] = *req.Status
|
||||
}
|
||||
if err := sensitivewordmod.Update(id, fields); err != nil {
|
||||
common.ServeJSON(ctx, stderr.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
common.ServeJSON(ctx, stderr.Success, nil)
|
||||
}
|
||||
|
||||
// Delete doc
|
||||
// @Summary 批量删除敏感词
|
||||
// @Description 根据ID列表批量删除敏感词
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body sensitivewordmod.DeleteReq true "ID列表"
|
||||
// @Success 200 {string} json "{"msg":"操作成功","data":{"deleted":0}}"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/delete [post]
|
||||
func Delete(ctx *gin.Context) {
|
||||
var req sensitivewordmod.DeleteReq
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
ids := make([]primitive.ObjectID, 0, len(req.IDs))
|
||||
for _, idStr := range req.IDs {
|
||||
id, err := primitive.ObjectIDFromHex(idStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, "no valid ids")
|
||||
return
|
||||
}
|
||||
cnt, err := sensitivewordmod.Delete(ids)
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
common.ServeJSON(ctx, stderr.Success, gin.H{"deleted": cnt})
|
||||
}
|
||||
|
||||
// Import doc
|
||||
// @Summary 批量导入敏感词
|
||||
// @Description 上传 xlsx 文件批量导入敏感词,列:A-一级分类 B-词条,同词条重复导入覆盖更新
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param upload formData file true "xlsx 文件,仅支持 .xls/.xlsx"
|
||||
// @Success 200 {string} json "{"msg":"操作成功","data":{"success":0,"skip":0}}"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/import [post]
|
||||
func Import(ctx *gin.Context) {
|
||||
file, fHeader, err := ctx.Request.FormFile("upload")
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, "请上传文件")
|
||||
return
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(fHeader.Filename))
|
||||
if ext != ".xlsx" && ext != ".xls" {
|
||||
common.ServeJSON(ctx, stderr.ErrMimeType, "仅支持 .xls/.xlsx 文件")
|
||||
return
|
||||
}
|
||||
|
||||
pwd, _ := os.Getwd()
|
||||
tmpPath := filepath.Join(pwd, "temp", fmt.Sprintf("sw_import_%d%s", time.Now().UnixNano(), ext))
|
||||
if err := os.MkdirAll(filepath.Dir(tmpPath), 0755); err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil)
|
||||
return
|
||||
}
|
||||
out, err := os.Create(tmpPath)
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil)
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err = out.ReadFrom(file); err != nil {
|
||||
out.Close()
|
||||
common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil)
|
||||
return
|
||||
}
|
||||
out.Close()
|
||||
|
||||
xlFile, err := xlsx.OpenFile(tmpPath)
|
||||
if err != nil {
|
||||
log.Error("sensitiveword import open file fail", log.E(err))
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, "文件解析失败")
|
||||
return
|
||||
}
|
||||
|
||||
var success, skip int
|
||||
for _, sheet := range xlFile.Sheets {
|
||||
cols, dataStart := findSensitiveCols(sheet)
|
||||
if dataStart < 0 {
|
||||
log.Warn("sensitiveword import: header row not found", log.Any("sheet", sheet.Name))
|
||||
continue
|
||||
}
|
||||
for rowIdx, row := range sheet.Rows {
|
||||
if rowIdx < dataStart {
|
||||
continue
|
||||
}
|
||||
category := getCellText(row, cols.Category)
|
||||
word := getCellText(row, cols.Word)
|
||||
|
||||
// 词条必填;分类缺省视为空字符串(不强制要求)
|
||||
if word == "" {
|
||||
skip++
|
||||
continue
|
||||
}
|
||||
|
||||
w := &sensitivewordmod.SensitiveWord{
|
||||
Category: category,
|
||||
Word: word,
|
||||
}
|
||||
if err := sensitivewordmod.UpsertByWord(w); err != nil {
|
||||
log.Warn("sensitiveword import upsert fail", log.Any("word", word), log.E(err))
|
||||
skip++
|
||||
continue
|
||||
}
|
||||
success++
|
||||
}
|
||||
}
|
||||
|
||||
common.ServeJSON(ctx, stderr.Success, gin.H{
|
||||
"success": success,
|
||||
"skip": skip,
|
||||
})
|
||||
}
|
||||
|
||||
// Export doc
|
||||
// @Summary 批量导出敏感词
|
||||
// @Description 导出敏感词为 xlsx 文件,格式与导入一致,支持按分类/风险等级/关键词筛选后导出
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Accept json
|
||||
// @Produce application/octet-stream
|
||||
// @Param category query string false "一级分类"
|
||||
// @Param keyword query string false "词条模糊搜索"
|
||||
// @Param status query int false "状态 1-启用 0-禁用"
|
||||
// @Success 200 {file} xlsx "xlsx 文件流"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/export [get]
|
||||
func Export(ctx *gin.Context) {
|
||||
var req sensitivewordmod.ListReq
|
||||
if err := ctx.ShouldBind(&req); err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrParamError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
list, err := sensitivewordmod.FindAll(&req)
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrDbQueryError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
f := xlsx.NewFile()
|
||||
sheet, err := f.AddSheet("敏感词库")
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil)
|
||||
return
|
||||
}
|
||||
|
||||
writeHeaderRow(sheet)
|
||||
for _, w := range list {
|
||||
row := sheet.AddRow()
|
||||
row.AddCell().Value = w.Category
|
||||
row.AddCell().Value = w.Word
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("敏感词库_%s.xlsx", time.Now().Format("20060102150405"))
|
||||
serveXlsx(ctx, f, fileName)
|
||||
}
|
||||
|
||||
// Template doc
|
||||
// @Summary 下载敏感词导入模版
|
||||
// @Description 返回仅含表头的 xlsx 模版文件,用户填写后可通过 import 接口导入
|
||||
// @Tags Web-SensitiveWord
|
||||
// @Produce application/octet-stream
|
||||
// @Success 200 {file} xlsx "xlsx 文件流"
|
||||
// @Failure 400 {string} json "{"msg":"操作失败"}"
|
||||
// @Router /api/web/admin/sensitive-word/template [get]
|
||||
func Template(ctx *gin.Context) {
|
||||
f := xlsx.NewFile()
|
||||
sheet, err := f.AddSheet("敏感词库")
|
||||
if err != nil {
|
||||
common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil)
|
||||
return
|
||||
}
|
||||
writeHeaderRow(sheet)
|
||||
serveXlsx(ctx, f, "敏感词库导入模版.xlsx")
|
||||
}
|
||||
|
||||
// writeHeaderRow 在 sheet 起始处写入表头行
|
||||
func writeHeaderRow(sheet *xlsx.Sheet) {
|
||||
row := sheet.AddRow()
|
||||
for _, h := range templateHeaders {
|
||||
row.AddCell().Value = h
|
||||
}
|
||||
}
|
||||
|
||||
// serveXlsx 把 xlsx 落到临时文件再以附件返回
|
||||
func serveXlsx(ctx *gin.Context, f *xlsx.File, fileName string) {
|
||||
pwd, _ := os.Getwd()
|
||||
tmpDir := filepath.Join(pwd, "temp")
|
||||
_ = os.MkdirAll(tmpDir, 0755)
|
||||
fpath := filepath.Join(tmpDir, fmt.Sprintf("%d_%s", time.Now().UnixNano(), fileName))
|
||||
if err := f.Save(fpath); err != nil {
|
||||
log.Error("sensitiveword save xlsx fail", log.E(err))
|
||||
common.ServeJSON(ctx, stderr.ErrServerUnavailable, nil)
|
||||
return
|
||||
}
|
||||
defer os.Remove(fpath)
|
||||
|
||||
ctx.Writer.WriteHeader(http.StatusOK)
|
||||
ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName))
|
||||
ctx.Header("Content-Type", "application/octet-stream")
|
||||
ctx.File(fpath)
|
||||
}
|
||||
|
||||
// sensitiveCols 通过表头智能识别出的列索引
|
||||
type sensitiveCols struct {
|
||||
Category int
|
||||
Word int
|
||||
}
|
||||
|
||||
// findSensitiveCols 扫描前几行找出各字段所在的列索引
|
||||
// 返回值: cols 列索引集合; dataStart 数据起始行号;-1 表示未识别到表头
|
||||
// 兼容 "词条/变体" 旧表头
|
||||
func findSensitiveCols(sheet *xlsx.Sheet) (sensitiveCols, int) {
|
||||
const maxHeaderScan = 5 // 表头一般在前几行
|
||||
for rowIdx, row := range sheet.Rows {
|
||||
if rowIdx > maxHeaderScan {
|
||||
break
|
||||
}
|
||||
cols := sensitiveCols{Category: -1, Word: -1}
|
||||
for cellIdx, cell := range row.Cells {
|
||||
text := strings.ReplaceAll(strings.TrimSpace(cell.String()), " ", "")
|
||||
text = strings.ReplaceAll(text, "\n", "")
|
||||
switch text {
|
||||
case "一级分类", "分类":
|
||||
cols.Category = cellIdx
|
||||
case "词条", "词条/变体":
|
||||
cols.Word = cellIdx
|
||||
}
|
||||
}
|
||||
// 词条列识别成功才视为表头行(分类列可选,缺省时入库为空)
|
||||
if cols.Word >= 0 {
|
||||
return cols, rowIdx + 1
|
||||
}
|
||||
}
|
||||
return sensitiveCols{}, -1
|
||||
}
|
||||
|
||||
// getCellText 安全地取出某列的文本,越界/未识别返回空串
|
||||
func getCellText(row *xlsx.Row, idx int) string {
|
||||
if idx < 0 || idx >= len(row.Cells) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(row.Cells[idx].String())
|
||||
}
|
||||
Reference in New Issue
Block a user