378 lines
11 KiB
Go
378 lines
11 KiB
Go
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
|
|
}
|