@@ -0,0 +1,377 @@
|
||||
package contentreviewmod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
mdbTask *db.MongoDB
|
||||
mdbIssue *db.MongoDB
|
||||
)
|
||||
|
||||
const (
|
||||
taskTable = models.ContentReviewTask
|
||||
issueTable = models.ContentReviewIssue
|
||||
)
|
||||
|
||||
func collTask(t *db.MongoTool) *db.MongoTool {
|
||||
if t == nil {
|
||||
return mdbTask.Coll(taskTable)
|
||||
}
|
||||
return t.Coll(taskTable)
|
||||
}
|
||||
|
||||
func collIssue(t *db.MongoTool) *db.MongoTool {
|
||||
if t == nil {
|
||||
return mdbIssue.Coll(issueTable)
|
||||
}
|
||||
return t.Coll(issueTable)
|
||||
}
|
||||
|
||||
func Init() {
|
||||
mdbTask = db.Init(taskTable)
|
||||
mdbIssue = db.Init(issueTable)
|
||||
initIndex()
|
||||
}
|
||||
|
||||
func initIndex() {
|
||||
taskIdx := []mongo.IndexModel{
|
||||
{Keys: bson.D{{Key: "status", Value: 1}}},
|
||||
{Keys: bson.D{{Key: "createdAt", Value: -1}}},
|
||||
}
|
||||
if _, err := collTask(nil).CreateIndex(taskIdx); err != nil {
|
||||
panic(fmt.Sprintf("%s set index err: %+v", taskTable, err))
|
||||
}
|
||||
issueIdx := []mongo.IndexModel{
|
||||
{Keys: bson.D{{Key: "taskId", Value: 1}, {Key: "createdAt", Value: 1}}},
|
||||
{Keys: bson.D{{Key: "targetId", Value: 1}}},
|
||||
}
|
||||
if _, err := collIssue(nil).CreateIndex(issueIdx); err != nil {
|
||||
panic(fmt.Sprintf("%s set index err: %+v", issueTable, err))
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTask 新建任务
|
||||
func CreateTask(t *ReviewTask) error {
|
||||
now := time.Now()
|
||||
t.CreatedAt = now
|
||||
t.Status = TaskStatusPending
|
||||
if _, err := collTask(nil).InsertOne(t); err != nil {
|
||||
log.Error(fmt.Sprintf("[contentreviewmod] CreateTask fail: %+v", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTaskByID 查询任务
|
||||
func GetTaskByID(id primitive.ObjectID) (*ReviewTask, error) {
|
||||
var t ReviewTask
|
||||
if err := collTask(nil).FindOneByID(&t, id); err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// HeartbeatTimeout 心跳超时阈值:超过该时间未更新心跳的 running 任务视为僵尸任务(进程崩溃后留下的)
|
||||
const HeartbeatTimeout = 3 * time.Minute
|
||||
|
||||
// PickNextTask 取最早的一个待处理任务:pending OR 心跳超时的 running
|
||||
// skd 单进程下足够;调用方需保证进程内串行执行
|
||||
func PickNextTask() (*ReviewTask, error) {
|
||||
threshold := time.Now().Add(-HeartbeatTimeout)
|
||||
cond := bson.M{
|
||||
"$or": []bson.M{
|
||||
{"status": TaskStatusPending},
|
||||
{"status": TaskStatusRunning, "heartbeatAt": bson.M{"$lt": threshold}},
|
||||
},
|
||||
}
|
||||
var t ReviewTask
|
||||
opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: 1}})
|
||||
if err := collTask(nil).FindOne(&t, cond, opts); err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// AcquireTask 标记任务进入运行态并刷心跳
|
||||
// 首次执行(pending)时设置 startedAt;续跑(running 心跳超时)保留原 startedAt
|
||||
func AcquireTask(id primitive.ObjectID, isResume bool) error {
|
||||
now := time.Now()
|
||||
set := bson.M{
|
||||
"status": TaskStatusRunning,
|
||||
"heartbeatAt": now,
|
||||
}
|
||||
if !isResume {
|
||||
set["startedAt"] = now
|
||||
}
|
||||
_, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateProgress 更新进度(批处理过程中调用),同时刷心跳和游标
|
||||
func UpdateProgress(id, lastTargetID primitive.ObjectID, checked, issues int64, total int64) error {
|
||||
set := bson.M{
|
||||
"checkedCount": checked,
|
||||
"issueCount": issues,
|
||||
"heartbeatAt": time.Now(),
|
||||
}
|
||||
if !lastTargetID.IsZero() {
|
||||
set["lastTargetId"] = lastTargetID
|
||||
}
|
||||
if total >= 0 {
|
||||
set["totalCount"] = total
|
||||
}
|
||||
_, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set})
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkFinished 标记完成
|
||||
func MarkFinished(id primitive.ObjectID, checked, issues int64) error {
|
||||
_, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{
|
||||
"status": TaskStatusFinished,
|
||||
"checkedCount": checked,
|
||||
"issueCount": issues,
|
||||
"finishedAt": time.Now(),
|
||||
}})
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkFailed 标记失败
|
||||
func MarkFailed(id primitive.ObjectID, errMsg string) error {
|
||||
_, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{
|
||||
"status": TaskStatusFailed,
|
||||
"errMsg": errMsg,
|
||||
"finishedAt": time.Now(),
|
||||
}})
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkTgSent 标记 TG 发送成功
|
||||
func MarkTgSent(id primitive.ObjectID) error {
|
||||
_, err := collTask(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"tgSent": true}})
|
||||
return err
|
||||
}
|
||||
|
||||
// ListTasks 任务分页
|
||||
func ListTasks(page, size int64, taskType string, status *int) ([]*ReviewTask, int64, error) {
|
||||
cond := bson.M{}
|
||||
if taskType != "" {
|
||||
cond["taskType"] = taskType
|
||||
}
|
||||
if status != nil {
|
||||
cond["status"] = *status
|
||||
}
|
||||
total, err := collTask(nil).Count(cond)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 20
|
||||
}
|
||||
opts := options.FindOptions{}
|
||||
opts.SetSort(bson.D{{Key: "createdAt", Value: -1}}).
|
||||
SetSkip((page - 1) * size).
|
||||
SetLimit(size)
|
||||
var list []*ReviewTask
|
||||
if err = collTask(nil).Find(&list, cond, &opts); err != nil {
|
||||
return nil, total, err
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
// InsertIssues 批量写入命中记录
|
||||
// 注意:底层 db.InsertMany 通过反射访问元素字段,要求 slice 元素必须是 struct 值
|
||||
// 而非指针,否则 FieldByName 会 panic;这里把 []*ReviewIssue 拷贝为 []ReviewIssue
|
||||
func InsertIssues(list []*ReviewIssue) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
flat := make([]ReviewIssue, len(list))
|
||||
for i, p := range list {
|
||||
p.CreatedAt = now
|
||||
flat[i] = *p
|
||||
}
|
||||
if _, err := collIssue(nil).InsertMany(&flat); err != nil {
|
||||
log.Error(fmt.Sprintf("[contentreviewmod] InsertIssues fail: %+v", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// incTaskResolved 原子累加任务的已解决命中计数
|
||||
func incTaskResolved(taskID primitive.ObjectID, delta int64) error {
|
||||
_, err := collTask(nil).UpdateOne(bson.M{"_id": taskID}, bson.M{"$inc": bson.M{"resolvedCount": delta}})
|
||||
return err
|
||||
}
|
||||
|
||||
// CountUnresolvedIssues 实时统计某任务下未解决(resolveStatus != 已解决)的记录数。
|
||||
// 列表页已改用 IssueCount-ResolvedCount 冗余差值,本函数保留用于计数重算/对账修复。
|
||||
func CountUnresolvedIssues(taskID primitive.ObjectID) (int64, error) {
|
||||
cond := bson.M{
|
||||
"taskId": taskID,
|
||||
"resolveStatus": bson.M{"$ne": ResolveStatusResolved},
|
||||
}
|
||||
return collIssue(nil).Count(cond)
|
||||
}
|
||||
|
||||
// GetIssueByID 按 ID 查命中记录
|
||||
func GetIssueByID(id primitive.ObjectID) (*ReviewIssue, error) {
|
||||
var i ReviewIssue
|
||||
if err := collIssue(nil).FindOneByID(&i, id); err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &i, nil
|
||||
}
|
||||
|
||||
// GetIssuesByIDs 批量按 ID 拉取命中记录;缺失/重复 ID 不会出现在返回中
|
||||
func GetIssuesByIDs(ids []primitive.ObjectID) ([]*ReviewIssue, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var list []*ReviewIssue
|
||||
if err := collIssue(nil).Find(&list, bson.M{"_id": bson.M{"$in": ids}}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// BatchOffShelfResolveIssues 批量把命中切到已解决(resolveAction=offshelf),
|
||||
// 调用方应先成功执行批量下架动作。
|
||||
// 仅 resolveStatus ∈ {Pending, Rejected} 的会被改动;perTaskResolved 是按 TaskID
|
||||
// 聚合的本批已解决条数,本函数原子地为每个任务 $inc resolvedCount。
|
||||
func BatchOffShelfResolveIssues(ids []primitive.ObjectID, perTaskResolved map[primitive.ObjectID]int64, operator string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
filter := bson.M{
|
||||
"_id": bson.M{"$in": ids},
|
||||
"resolveStatus": bson.M{"$in": []int{
|
||||
ResolveStatusPending,
|
||||
ResolveStatusRejected,
|
||||
}},
|
||||
}
|
||||
update := bson.M{"$set": bson.M{
|
||||
"resolveStatus": ResolveStatusResolved,
|
||||
"resolveAction": ResolveActionOffShelf,
|
||||
"submittedAt": now,
|
||||
"submittedBy": operator,
|
||||
"resolvedAt": now,
|
||||
}}
|
||||
if _, err := collIssue(nil).UpdateMany(filter, update); err != nil {
|
||||
return err
|
||||
}
|
||||
for taskID, n := range perTaskResolved {
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
if err := incTaskResolved(taskID, n); err != nil {
|
||||
log.Warn(fmt.Sprintf("[contentreviewmod] inc resolvedCount fail: taskId=%s delta=%d err=%+v", taskID.Hex(), n, err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveIssue 管理员提交修改并直接标记已解决
|
||||
// 允许从未解决(0)或已搁置(1)切到已解决(2);调用方应先成功写回数据源
|
||||
// 状态确实发生转换(ModifiedCount>0)时,原子累加所属任务的已解决计数
|
||||
func ResolveIssue(id, taskID primitive.ObjectID, submittedTitle, submittedContent, submittedRichText, operator string) (bool, error) {
|
||||
now := time.Now()
|
||||
filter := bson.M{
|
||||
"_id": id,
|
||||
"resolveStatus": bson.M{"$in": []int{
|
||||
ResolveStatusPending,
|
||||
ResolveStatusRejected,
|
||||
}},
|
||||
}
|
||||
update := bson.M{"$set": bson.M{
|
||||
"resolveStatus": ResolveStatusResolved,
|
||||
"resolveAction": ResolveActionText,
|
||||
"submittedTitle": submittedTitle,
|
||||
"submittedContent": submittedContent,
|
||||
"submittedRichText": submittedRichText,
|
||||
"submittedAt": now,
|
||||
"submittedBy": operator,
|
||||
"resolvedAt": now,
|
||||
}}
|
||||
res, err := collIssue(nil).UpdateOne(filter, update)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if res.ModifiedCount > 0 {
|
||||
if err := incTaskResolved(taskID, 1); err != nil {
|
||||
log.Warn(fmt.Sprintf("[contentreviewmod] inc resolvedCount fail: taskId=%s err=%+v", taskID.Hex(), err))
|
||||
}
|
||||
}
|
||||
return res.ModifiedCount > 0, nil
|
||||
}
|
||||
|
||||
// RejectIssue 搁置:记录原因并把状态切到 已搁置(1)
|
||||
// 只在当前状态为 未解决(0)时允许;reason 必填便于追溯
|
||||
func RejectIssue(id primitive.ObjectID, operator, reason string) (bool, error) {
|
||||
filter := bson.M{
|
||||
"_id": id,
|
||||
"resolveStatus": ResolveStatusPending,
|
||||
}
|
||||
update := bson.M{"$set": bson.M{
|
||||
"resolveStatus": ResolveStatusRejected,
|
||||
"reviewedAt": time.Now(),
|
||||
"reviewedBy": operator,
|
||||
"rejectReason": reason,
|
||||
}}
|
||||
res, err := collIssue(nil).UpdateOne(filter, update)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.ModifiedCount > 0, nil
|
||||
}
|
||||
|
||||
// ListIssues 命中记录分页;resolveStatus 传 nil 表示全部
|
||||
func ListIssues(taskID primitive.ObjectID, page, size int64, resolveStatus *int) ([]*ReviewIssue, int64, error) {
|
||||
cond := bson.M{"taskId": taskID}
|
||||
if resolveStatus != nil {
|
||||
cond["resolveStatus"] = *resolveStatus
|
||||
}
|
||||
total, err := collIssue(nil).Count(cond)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 20
|
||||
}
|
||||
opts := options.FindOptions{}
|
||||
opts.SetSort(bson.D{{Key: "createdAt", Value: 1}}).
|
||||
SetSkip((page - 1) * size).
|
||||
SetLimit(size)
|
||||
var list []*ReviewIssue
|
||||
if err = collIssue(nil).Find(&list, cond, &opts); err != nil {
|
||||
return nil, total, err
|
||||
}
|
||||
return list, total, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package contentreviewmod
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// TaskType 任务类型常量
|
||||
const (
|
||||
TaskTypeVideo = "VIDEO" // 视频帖子(扫描 VideoModel 表)
|
||||
TaskTypeACG = "ACG" // ACG 漫画/小说(扫描 Media + MediaContent 表)
|
||||
)
|
||||
|
||||
// TaskStatus 任务状态机
|
||||
//
|
||||
// 0 待执行 → 1 执行中 → 2 已完成
|
||||
// ↓
|
||||
// 3 失败(panic 或异常)
|
||||
//
|
||||
// 心跳超时的 1 执行中 任务,下次 cron tick 会被自动接管续跑
|
||||
const (
|
||||
TaskStatusPending = 0 // 待执行:刚创建,等待 cron 调度
|
||||
TaskStatusRunning = 1 // 执行中:cron 已挑出并正在扫描
|
||||
TaskStatusFinished = 2 // 已完成:扫描完所有目标
|
||||
TaskStatusFailed = 3 // 失败:执行中遇到不可恢复错误
|
||||
)
|
||||
|
||||
// IssueTargetType 命中记录关联的目标对象类型(字符串枚举)
|
||||
type IssueTargetType string
|
||||
|
||||
const (
|
||||
IssueTargetVideo IssueTargetType = "VIDEO" // 命中视频帖子(VideoModel)
|
||||
IssueTargetACGMedia IssueTargetType = "ACG_MEDIA" // 命中 ACG 的 Media 自身字段
|
||||
IssueTargetACGContent IssueTargetType = "ACG_CONTENT" // 命中 ACG 的 MediaContent 章节
|
||||
)
|
||||
|
||||
// ResolveStatus 命中记录的解决状态机
|
||||
//
|
||||
// 0 未解决 ─── Resolve(提交修改+写回数据源) ─────────→ 2 已解决
|
||||
// │ ↑
|
||||
// └── Reject(搁置,记录原因) ──→ 1 已搁置 ── Resolve ────┘
|
||||
//
|
||||
// 未解决 / 已搁置 都视为"待解决"(unresolvedCount 累计)
|
||||
const (
|
||||
ResolveStatusPending = 0 // 未解决:初始状态,等待管理员处理
|
||||
ResolveStatusRejected = 1 // 已搁置:管理员审阅后决定暂不修改数据源,已记录搁置原因;仍可后续 Resolve
|
||||
ResolveStatusResolved = 2 // 已解决:管理员提交修改并写回数据源,闭环
|
||||
)
|
||||
|
||||
// SensitiveWordSnap 任务发起时刻对敏感词库的快照
|
||||
// 防止敏感词库后续被修改影响历史任务的可追溯性
|
||||
type SensitiveWordSnap struct {
|
||||
Word string `json:"word" bson:"word"` // 词条
|
||||
Category string `json:"category" bson:"category"` // 一级分类
|
||||
}
|
||||
|
||||
// ReviewTask 内容审查任务记录
|
||||
// 由管理员发起,skd 服务的定时调度器异步扫描;同一时刻只跑一个任务
|
||||
type ReviewTask struct {
|
||||
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 任务 ID
|
||||
TaskType string `json:"taskType" bson:"taskType"` // 任务类型:VIDEO / ACG
|
||||
SnapshotWords []SensitiveWordSnap `json:"snapshotWords" bson:"snapshotWords"` // 发起时刻的敏感词快照
|
||||
Status int `json:"status" bson:"status"` // 任务状态 见 TaskStatusXxx
|
||||
TotalCount int64 `json:"totalCount" bson:"totalCount"` // 待扫描总数(启动时初始化)
|
||||
CheckedCount int64 `json:"checkedCount" bson:"checkedCount"` // 已扫描数量
|
||||
IssueCount int64 `json:"issueCount" bson:"issueCount"` // 累计命中记录数
|
||||
// ResolvedCount 已解决命中数的冗余计数:每次 Resolve 成功 $inc +1。
|
||||
// 待解决数 = IssueCount - ResolvedCount,列表页读时算差值,避免对每个任务实时 count。
|
||||
ResolvedCount int64 `json:"resolvedCount" bson:"resolvedCount"`
|
||||
LastTargetID primitive.ObjectID `json:"lastTargetId" bson:"lastTargetId"` // 已扫描的最后一条目标 _id,断点续跑游标
|
||||
HeartbeatAt time.Time `json:"heartbeatAt" bson:"heartbeatAt"` // 心跳:每批结束刷新,超时视为僵尸任务可被接管
|
||||
TgSent bool `json:"tgSent" bson:"tgSent"` // 完成通知 TG 是否发送成功
|
||||
Operator string `json:"operator" bson:"operator"` // 发起人账号
|
||||
ErrMsg string `json:"errMsg" bson:"errMsg"` // 失败原因(status=3 时)
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 任务创建时间
|
||||
StartedAt time.Time `json:"startedAt" bson:"startedAt"` // 首次启动时间(续跑不重置)
|
||||
FinishedAt time.Time `json:"finishedAt" bson:"finishedAt"` // 完成时间
|
||||
}
|
||||
|
||||
// ReviewIssue 命中问题记录(一条 issue 代表一个目标对象的全部字段命中信息)
|
||||
//
|
||||
// VIDEO: Title/Content/RichText 字段对应 VideoModel.Title/Content/RichText
|
||||
// ACG_MEDIA: Title=Media.Title, Content=Media.Summary, RichText 不使用
|
||||
// ACG_CONTENT: Title=MediaContent.Name, Content=MediaContent.Text, MediaID 关联回 Media._id
|
||||
//
|
||||
// Title/Content/Tags/RichText 字段存放经过 <mark>...</mark> 高亮标记后的文本
|
||||
type ReviewIssue struct {
|
||||
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` // 记录 ID
|
||||
TaskID primitive.ObjectID `json:"taskId" bson:"taskId"` // 所属任务 ID
|
||||
TargetType IssueTargetType `json:"targetType" bson:"targetType"` // 目标对象类型 见 IssueTargetXxx
|
||||
TargetID primitive.ObjectID `json:"targetId" bson:"targetId"` // 目标对象 _id
|
||||
MediaID primitive.ObjectID `json:"mediaId,omitempty" bson:"mediaId,omitempty"` // ACG_CONTENT 时关联的 Media._id
|
||||
|
||||
Title string `json:"title" bson:"title"` // 高亮后的标题(VIDEO/ACG_MEDIA.Title 或 ACG_CONTENT.Name)
|
||||
Content string `json:"content" bson:"content"` // 高亮后的内容(VIDEO.Content / ACG_MEDIA.Summary / ACG_CONTENT.Text)
|
||||
Tags string `json:"tags" bson:"tags"` // 高亮后的标签("标签1、标签2")
|
||||
RichText string `json:"richText" bson:"richText"` // 高亮后的富文本(仅 VIDEO 有)
|
||||
Description string `json:"description" bson:"description"` // 问题描述,形如"标题命中 词1、词2\n标签命中 词3"
|
||||
HitWords []string `json:"hitWords" bson:"hitWords"` // 去重后的全部命中词
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 记录创建时间
|
||||
|
||||
// —— 解决/审核流程 ——
|
||||
ResolveStatus int `json:"resolveStatus" bson:"resolveStatus"` // 解决状态 见 ResolveStatusXxx
|
||||
SubmittedTitle string `json:"submittedTitle" bson:"submittedTitle"` // 管理员提交修改后的标题(待审核内容)
|
||||
SubmittedContent string `json:"submittedContent" bson:"submittedContent"` // 管理员提交修改后的内容
|
||||
SubmittedRichText string `json:"submittedRichText" bson:"submittedRichText"` // 管理员提交修改后的富文本(仅 VIDEO)
|
||||
SubmittedAt time.Time `json:"submittedAt" bson:"submittedAt"` // 最近一次提交时间
|
||||
SubmittedBy string `json:"submittedBy" bson:"submittedBy"` // 最近一次提交人账号
|
||||
RejectReason string `json:"rejectReason" bson:"rejectReason"` // 审核打回原因(resolveStatus 退回 0 时保留)
|
||||
ReviewedAt time.Time `json:"reviewedAt" bson:"reviewedAt"` // 最近一次审核时间
|
||||
ReviewedBy string `json:"reviewedBy" bson:"reviewedBy"` // 最近一次审核人账号
|
||||
ResolvedAt time.Time `json:"resolvedAt" bson:"resolvedAt"` // 最终解决时间(resolveStatus=2 时)
|
||||
ResolveAction string `json:"resolveAction" bson:"resolveAction"` // 解决方式:空/"text"-文本修改 "offshelf"-直接下架
|
||||
}
|
||||
|
||||
// 解决方式取值
|
||||
const (
|
||||
ResolveActionText = "text" // 提交文本修改
|
||||
ResolveActionOffShelf = "offshelf" // 批量下架处置
|
||||
)
|
||||
Reference in New Issue
Block a user