Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
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, " "))
}