feat: add batch content report processing
This commit is contained in:
@@ -3166,6 +3166,163 @@ func (s *super) ProcessContentReport(ctx context.Context, operatorID, id int64,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *super) BatchProcessContentReports(ctx context.Context, operatorID int64, form *super_dto.SuperContentReportBatchProcessForm) error {
|
||||
if operatorID == 0 {
|
||||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||||
}
|
||||
if form == nil {
|
||||
return errorx.ErrBadRequest.WithMsg("处理参数不能为空")
|
||||
}
|
||||
|
||||
action := strings.ToLower(strings.TrimSpace(form.Action))
|
||||
if action != "approve" && action != "reject" {
|
||||
return errorx.ErrBadRequest.WithMsg("处理动作非法")
|
||||
}
|
||||
|
||||
contentAction := strings.ToLower(strings.TrimSpace(form.ContentAction))
|
||||
if action == "approve" {
|
||||
if contentAction == "" {
|
||||
contentAction = "ignore"
|
||||
}
|
||||
switch contentAction {
|
||||
case "block", "unpublish", "ignore":
|
||||
default:
|
||||
return errorx.ErrBadRequest.WithMsg("内容处置动作非法")
|
||||
}
|
||||
} else {
|
||||
contentAction = "ignore"
|
||||
}
|
||||
|
||||
// 去重并过滤非法ID,避免批量处理出现空指令。
|
||||
unique := make(map[int64]struct{})
|
||||
reportIDs := make([]int64, 0, len(form.ReportIDs))
|
||||
for _, id := range form.ReportIDs {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := unique[id]; ok {
|
||||
continue
|
||||
}
|
||||
unique[id] = struct{}{}
|
||||
reportIDs = append(reportIDs, id)
|
||||
}
|
||||
if len(reportIDs) == 0 {
|
||||
return errorx.ErrBadRequest.WithMsg("举报ID不能为空")
|
||||
}
|
||||
|
||||
status := "approved"
|
||||
if action == "reject" {
|
||||
status = "rejected"
|
||||
}
|
||||
handledReason := strings.TrimSpace(form.Reason)
|
||||
handledAt := time.Now()
|
||||
|
||||
var reports []*models.ContentReport
|
||||
contentMap := make(map[int64]*models.Content)
|
||||
// 事务内批量更新举报处理结果及关联内容状态,保证一致性。
|
||||
err := models.Q.Transaction(func(tx *models.Query) error {
|
||||
reportTbl, reportQuery := tx.ContentReport.QueryContext(ctx)
|
||||
list, err := reportQuery.Where(reportTbl.ID.In(reportIDs...)).Find()
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if len(list) != len(reportIDs) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("部分举报不存在")
|
||||
}
|
||||
for _, report := range list {
|
||||
if strings.TrimSpace(report.Status) != "" && report.Status != "pending" {
|
||||
return errorx.ErrStatusConflict.WithMsg("仅可处理待处理举报")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = reportQuery.Where(reportTbl.ID.In(reportIDs...)).UpdateSimple(
|
||||
reportTbl.Status.Value(status),
|
||||
reportTbl.HandledBy.Value(operatorID),
|
||||
reportTbl.HandledAction.Value(contentAction),
|
||||
reportTbl.HandledReason.Value(handledReason),
|
||||
reportTbl.HandledAt.Value(handledAt),
|
||||
)
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
if action == "approve" {
|
||||
contentIDs := make([]int64, 0, len(list))
|
||||
for _, report := range list {
|
||||
contentIDs = append(contentIDs, report.ContentID)
|
||||
}
|
||||
contentTbl, contentQuery := tx.Content.QueryContext(ctx)
|
||||
contentQueryUnscoped := contentQuery.Unscoped()
|
||||
contents, err := contentQueryUnscoped.Where(contentTbl.ID.In(contentIDs...)).Find()
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, content := range contents {
|
||||
contentMap[content.ID] = content
|
||||
}
|
||||
|
||||
if contentAction != "ignore" {
|
||||
var nextStatus consts.ContentStatus
|
||||
switch contentAction {
|
||||
case "block":
|
||||
nextStatus = consts.ContentStatusBlocked
|
||||
case "unpublish":
|
||||
nextStatus = consts.ContentStatusUnpublished
|
||||
}
|
||||
if nextStatus != "" && len(contents) > 0 {
|
||||
if _, err := contentQuery.Where(contentTbl.ID.In(contentIDs...)).UpdateSimple(
|
||||
contentTbl.Status.Value(nextStatus),
|
||||
); err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reports = list
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if action == "approve" {
|
||||
title := "内容举报成立"
|
||||
actionLabel := "不处理内容"
|
||||
switch contentAction {
|
||||
case "block":
|
||||
actionLabel = "封禁内容"
|
||||
case "unpublish":
|
||||
actionLabel = "下架内容"
|
||||
}
|
||||
for _, report := range reports {
|
||||
content := contentMap[report.ContentID]
|
||||
if content == nil {
|
||||
continue
|
||||
}
|
||||
detail := "内容《" + content.Title + "》举报成立,处理结果:" + actionLabel
|
||||
if handledReason != "" {
|
||||
detail += "。说明:" + handledReason
|
||||
}
|
||||
if Notification != nil {
|
||||
_ = Notification.Send(ctx, content.TenantID, content.UserID, string(consts.NotificationTypeAudit), title, detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Audit != nil {
|
||||
detail := "处理内容举报:" + action + " / " + contentAction
|
||||
if handledReason != "" {
|
||||
detail += " / " + handledReason
|
||||
}
|
||||
for _, report := range reports {
|
||||
Audit.Log(ctx, report.TenantID, operatorID, "process_content_report", cast.ToString(report.ID), detail)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *super) UpdateContentStatus(ctx context.Context, tenantID, contentID int64, form *super_dto.SuperTenantContentStatusUpdateForm) error {
|
||||
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||
_, err := q.Where(tbl.ID.Eq(contentID), tbl.TenantID.Eq(tenantID)).Update(tbl.Status, consts.ContentStatus(form.Status))
|
||||
|
||||
@@ -396,6 +396,107 @@ func (s *SuperTestSuite) Test_ContentReportGovernance() {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SuperTestSuite) Test_BatchProcessContentReports() {
|
||||
Convey("BatchProcessContentReports", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
database.Truncate(ctx, s.DB, models.TableNameContentReport, models.TableNameContent, models.TableNameTenant, models.TableNameUser)
|
||||
|
||||
owner := &models.User{Username: "batch_report_owner"}
|
||||
reporter := &models.User{Username: "batch_reporter"}
|
||||
admin := &models.User{Username: "batch_report_admin"}
|
||||
models.UserQuery.WithContext(ctx).Create(owner, reporter, admin)
|
||||
|
||||
tenant := &models.Tenant{UserID: owner.ID, Code: "t-report-batch", Name: "Report Batch Tenant", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(tenant)
|
||||
|
||||
contentA := &models.Content{
|
||||
TenantID: tenant.ID,
|
||||
UserID: owner.ID,
|
||||
Title: "Batch Report A",
|
||||
Status: consts.ContentStatusPublished,
|
||||
}
|
||||
contentB := &models.Content{
|
||||
TenantID: tenant.ID,
|
||||
UserID: owner.ID,
|
||||
Title: "Batch Report B",
|
||||
Status: consts.ContentStatusPublished,
|
||||
}
|
||||
models.ContentQuery.WithContext(ctx).Create(contentA, contentB)
|
||||
|
||||
Convey("should batch approve reports and unpublish contents", func() {
|
||||
reportA := &models.ContentReport{
|
||||
TenantID: tenant.ID,
|
||||
ContentID: contentA.ID,
|
||||
ReporterID: reporter.ID,
|
||||
Reason: "spam",
|
||||
Detail: "批量举报A",
|
||||
Status: "pending",
|
||||
}
|
||||
reportB := &models.ContentReport{
|
||||
TenantID: tenant.ID,
|
||||
ContentID: contentB.ID,
|
||||
ReporterID: reporter.ID,
|
||||
Reason: "abuse",
|
||||
Detail: "批量举报B",
|
||||
Status: "pending",
|
||||
}
|
||||
models.ContentReportQuery.WithContext(ctx).Create(reportA, reportB)
|
||||
|
||||
err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{
|
||||
ReportIDs: []int64{reportA.ID, reportB.ID},
|
||||
Action: "approve",
|
||||
ContentAction: "unpublish",
|
||||
Reason: "集中处理",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
reloadedA, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportA.ID)).First()
|
||||
So(err, ShouldBeNil)
|
||||
So(reloadedA.Status, ShouldEqual, "approved")
|
||||
So(reloadedA.HandledBy, ShouldEqual, admin.ID)
|
||||
So(reloadedA.HandledAction, ShouldEqual, "unpublish")
|
||||
So(reloadedA.HandledReason, ShouldEqual, "集中处理")
|
||||
So(reloadedA.HandledAt.IsZero(), ShouldBeFalse)
|
||||
|
||||
reloadedB, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportB.ID)).First()
|
||||
So(err, ShouldBeNil)
|
||||
So(reloadedB.Status, ShouldEqual, "approved")
|
||||
So(reloadedB.HandledAction, ShouldEqual, "unpublish")
|
||||
|
||||
contentReloadA, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentA.ID)).First()
|
||||
So(err, ShouldBeNil)
|
||||
So(contentReloadA.Status, ShouldEqual, consts.ContentStatusUnpublished)
|
||||
|
||||
contentReloadB, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentB.ID)).First()
|
||||
So(err, ShouldBeNil)
|
||||
So(contentReloadB.Status, ShouldEqual, consts.ContentStatusUnpublished)
|
||||
})
|
||||
|
||||
Convey("should reject when report already handled", func() {
|
||||
report := &models.ContentReport{
|
||||
TenantID: tenant.ID,
|
||||
ContentID: contentA.ID,
|
||||
ReporterID: reporter.ID,
|
||||
Reason: "other",
|
||||
Detail: "已处理",
|
||||
Status: "approved",
|
||||
}
|
||||
models.ContentReportQuery.WithContext(ctx).Create(report)
|
||||
|
||||
err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{
|
||||
ReportIDs: []int64{report.ID},
|
||||
Action: "reject",
|
||||
Reason: "重复提交",
|
||||
})
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
var appErr *errorx.AppError
|
||||
So(errors.As(err, &appErr), ShouldBeTrue)
|
||||
So(appErr.Code, ShouldEqual, errorx.ErrStatusConflict.Code)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SuperTestSuite) Test_FinanceAnomalies() {
|
||||
Convey("Finance Anomalies", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
|
||||
Reference in New Issue
Block a user