Files
huangguo_server/models/v/contentreviewmod/crud.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

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
}