@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user