Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

171 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package vidser
import (
"errors"
"fmt"
"strings"
"91porn-server/common/log"
"91porn-server/models/v/sensitivewordmod"
"91porn-server/models/v/vidmod"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// CheckShelvingFields 直接对一组文本做敏感词命中检测;用于单帖编辑场景,
// 调用方负责把请求字段与 DB 现值合并后传入。
// 命中规则与 CheckShelvingByIDs 一致:Title / Content / RichText 任一命中即视为违规。
func CheckShelvingFields(id primitive.ObjectID, title, content, richText string) error {
terms := loadEnabledSensitiveTerms()
if len(terms) == 0 {
return nil
}
titleHits := findSensitiveHits(title, terms)
contentHits := findSensitiveHits(content, terms)
richHits := findSensitiveHits(richText, terms)
if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 {
return nil
}
return errors.New(formatHitLine(id, title, titleHits, contentHits, richHits))
}
// CheckShelvingByIDs 上架前敏感词命中检测。
//
// 与 skd/service/contentreviewser VIDEO 任务的扫描规则保持一致:
// 对每条视频的 Title / Content / RichText 三个字段做 substring 命中检测,
// 任一字段命中启用状态的敏感词词条即视为违规,不予上架。
//
// 返回值:
// - nil —— 全部通过或敏感词库为空
// - non-nil err —— 至少一条命中,err.Error() 已按视频聚合
//
// 多条命中时,每条视频独占一行,行内字段按"标题命中 / 内容命中 / 富文本命中"顺序拼接。
func CheckShelvingByIDs(ids []primitive.ObjectID) error {
if len(ids) == 0 {
return nil
}
terms := loadEnabledSensitiveTerms()
if len(terms) == 0 {
return nil
}
videos, err := vidmod.GetVideoListByIDsNoStatus(ids)
if err != nil {
log.Error("vidser.CheckShelvingByIDs query videos fail", log.Any("ids", ids), log.E(err))
return errors.New("查询视频信息失败")
}
lines := make([]string, 0)
for _, v := range videos {
titleHits := findSensitiveHits(v.Title, terms)
contentHits := findSensitiveHits(v.Content, terms)
richHits := findSensitiveHits(v.RichText, terms)
if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 {
continue
}
lines = append(lines, formatHitLine(v.ID, v.Title, titleHits, contentHits, richHits))
}
if len(lines) == 0 {
return nil
}
return errors.New(strings.Join(lines, "\n"))
}
// LoadEnabledSensitiveTerms 启用状态敏感词词条加载(代理 sensitivewordmod.LoadEnabledTerms
// 保留此处签名以兼容现有 laosijiser / tianyuctrl 等调用方
func LoadEnabledSensitiveTerms() []string {
return sensitivewordmod.LoadEnabledTerms()
}
// loadEnabledSensitiveTerms 内部短名别名
func loadEnabledSensitiveTerms() []string {
return sensitivewordmod.LoadEnabledTerms()
}
// SensitiveHit 单条命中结果,用于批量同步接口(老司机/天宇媒资库)返回给前端
type SensitiveHit struct {
SourceID string `json:"sourceId"` // 来源 id(如 laosiji 资源 id / tianyu fileId
Title string `json:"title"` // 标题
Detail string `json:"detail"` // "标题命中 XX 内容命中 YY 富文本命中 ZZ"
}
// CheckTextHits 对一组文本做敏感词命中检测,命中返回 *SensitiveHit,未命中返回 nil。
// 与 skd 内容审查任务的匹配规则保持一致:任一字段命中即视为命中。
// 调用方在入库前调用,命中后应将记录的 status 强制为"未上架"语义的值。
//
// 词条由外部传入,便于批量场景一次加载、多次匹配;传 nil 或空切片视为词库为空,直接返回 nil。
func CheckTextHits(terms []string, sourceID, title, content, richText string) *SensitiveHit {
if len(terms) == 0 {
return nil
}
titleHits := findSensitiveHits(title, terms)
contentHits := findSensitiveHits(content, terms)
richHits := findSensitiveHits(richText, terms)
if len(titleHits) == 0 && len(contentHits) == 0 && len(richHits) == 0 {
return nil
}
parts := make([]string, 0, 3)
if len(titleHits) > 0 {
parts = append(parts, "标题命中 "+strings.Join(titleHits, "、"))
}
if len(contentHits) > 0 {
parts = append(parts, "内容命中 "+strings.Join(contentHits, "、"))
}
if len(richHits) > 0 {
parts = append(parts, "富文本命中 "+strings.Join(richHits, "、"))
}
return &SensitiveHit{
SourceID: sourceID,
Title: title,
Detail: strings.Join(parts, " "),
}
}
// SensitiveForcedOfflineTip 写库后给管理员的统一提示文案
const SensitiveForcedOfflineTip = "由于部分数据内容命中敏感词,状态强制为下架"
// findSensitiveHits substring 命中检测的内部别名,代理 sensitivewordmod.MatchHits
func findSensitiveHits(input string, terms []string) []string {
return sensitivewordmod.MatchHits(input, terms)
}
// shouldCheckSensitiveOnUpdate 判断单帖编辑 UpdateVidInfo 是否需要做敏感词校验
//
// 1. status 被切到 上架(CheckPass=1) / 上架并免费(Free=3) —— 上架转换
// 2. 当前已是上架态(1/3) 且本次更新涉及 Title/Content/RichText —— 已上架内容改动
func shouldCheckSensitiveOnUpdate(vidInfo vidmod.VideoModel, req vidmod.EditReq) bool {
if req.Status != nil && (*req.Status == vidmod.CheckPass || *req.Status == vidmod.Free) {
return true
}
isCurrentlyShelved := vidInfo.Status == vidmod.CheckPass || vidInfo.Status == vidmod.Free
touchesText := req.Title != nil || req.Content != nil || req.RichText != nil
return isCurrentlyShelved && touchesText
}
// parseObjectIDs 把十六进制 id 字符串数组转 ObjectID,非法 id 静默丢弃
func parseObjectIDs(ids []string) []primitive.ObjectID {
out := make([]primitive.ObjectID, 0, len(ids))
for _, s := range ids {
oid, err := primitive.ObjectIDFromHex(s)
if err != nil {
continue
}
out = append(out, oid)
}
return out
}
// formatHitLine 按需求格式拼接单条命中描述:
// "id:xxxx 对象标题 XXXXXX。 标题命中 XX 内容命中 XX 富文本命中 XX,不予上架成功"
func formatHitLine(id primitive.ObjectID, title string, titleHits, contentHits, richHits []string) string {
parts := make([]string, 0, 3)
if len(titleHits) > 0 {
parts = append(parts, "标题命中 "+strings.Join(titleHits, "、"))
}
if len(contentHits) > 0 {
parts = append(parts, "内容命中 "+strings.Join(contentHits, "、"))
}
if len(richHits) > 0 {
parts = append(parts, "富文本命中 "+strings.Join(richHits, "、"))
}
return fmt.Sprintf("id:%s 对象标题 %s。 %s,不予上架成功", id.Hex(), title, strings.Join(parts, " "))
}