feat: add batch content report processing
This commit is contained in:
@@ -45,3 +45,19 @@ func (c *contentReports) List(ctx fiber.Ctx, filter *dto.SuperContentReportListF
|
||||
func (c *contentReports) Process(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperContentReportProcessForm) error {
|
||||
return services.Super.ProcessContentReport(ctx, user.ID, id, form)
|
||||
}
|
||||
|
||||
// Batch process content reports
|
||||
//
|
||||
// @Router /super/v1/content-reports/process/batch [post]
|
||||
// @Summary Batch process content reports
|
||||
// @Description Batch process content report records
|
||||
// @Tags Content
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.SuperContentReportBatchProcessForm true "Batch process form"
|
||||
// @Success 200 {string} string "Processed"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *contentReports) BatchProcess(ctx fiber.Ctx, user *models.User, form *dto.SuperContentReportBatchProcessForm) error {
|
||||
return services.Super.BatchProcessContentReports(ctx, user.ID, form)
|
||||
}
|
||||
|
||||
@@ -98,3 +98,15 @@ type SuperContentReportProcessForm struct {
|
||||
// Reason 处理说明(可选,用于审计记录)。
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// SuperContentReportBatchProcessForm 超管内容举报批量处理表单。
|
||||
type SuperContentReportBatchProcessForm struct {
|
||||
// ReportIDs 待处理举报ID列表。
|
||||
ReportIDs []int64 `json:"report_ids" validate:"required,min=1,dive,gt=0"`
|
||||
// Action 处理动作(approve/reject)。
|
||||
Action string `json:"action" validate:"required,oneof=approve reject"`
|
||||
// ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。
|
||||
ContentAction string `json:"content_action"`
|
||||
// Reason 处理说明(可选,用于审计记录)。
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
@@ -108,6 +108,12 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.SuperContentReportProcessForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /super/v1/content-reports/process/batch -> contentReports.BatchProcess")
|
||||
router.Post("/super/v1/content-reports/process/batch"[len(r.Path()):], Func2(
|
||||
r.contentReports.BatchProcess,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.SuperContentReportBatchProcessForm]("form"),
|
||||
))
|
||||
// Register routes for controller: contents
|
||||
r.log.Debugf("Registering route: Get /super/v1/contents -> contents.List")
|
||||
router.Get("/super/v1/contents"[len(r.Path()):], DataFunc1(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -392,6 +392,40 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/content-reports/process/batch": {
|
||||
"post": {
|
||||
"description": "Batch process content report records",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Content"
|
||||
],
|
||||
"summary": "Batch process content reports",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Batch process form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.SuperContentReportBatchProcessForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Processed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/content-reports/{id}/process": {
|
||||
"post": {
|
||||
"description": "Process a content report record",
|
||||
@@ -8155,6 +8189,39 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SuperContentReportBatchProcessForm": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"action",
|
||||
"report_ids"
|
||||
],
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "Action 处理动作(approve/reject)。",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"approve",
|
||||
"reject"
|
||||
]
|
||||
},
|
||||
"content_action": {
|
||||
"description": "ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。",
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason 处理说明(可选,用于审计记录)。",
|
||||
"type": "string"
|
||||
},
|
||||
"report_ids": {
|
||||
"description": "ReportIDs 待处理举报ID列表。",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SuperContentReportItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -386,6 +386,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/content-reports/process/batch": {
|
||||
"post": {
|
||||
"description": "Batch process content report records",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Content"
|
||||
],
|
||||
"summary": "Batch process content reports",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Batch process form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.SuperContentReportBatchProcessForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Processed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/content-reports/{id}/process": {
|
||||
"post": {
|
||||
"description": "Process a content report record",
|
||||
@@ -8149,6 +8183,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SuperContentReportBatchProcessForm": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"action",
|
||||
"report_ids"
|
||||
],
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "Action 处理动作(approve/reject)。",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"approve",
|
||||
"reject"
|
||||
]
|
||||
},
|
||||
"content_action": {
|
||||
"description": "ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。",
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason 处理说明(可选,用于审计记录)。",
|
||||
"type": "string"
|
||||
},
|
||||
"report_ids": {
|
||||
"description": "ReportIDs 待处理举报ID列表。",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SuperContentReportItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1421,6 +1421,30 @@ definitions:
|
||||
- content_ids
|
||||
- status
|
||||
type: object
|
||||
dto.SuperContentReportBatchProcessForm:
|
||||
properties:
|
||||
action:
|
||||
description: Action 处理动作(approve/reject)。
|
||||
enum:
|
||||
- approve
|
||||
- reject
|
||||
type: string
|
||||
content_action:
|
||||
description: ContentAction 内容处置动作(block/unpublish/ignore),仅在 approve 时生效。
|
||||
type: string
|
||||
reason:
|
||||
description: Reason 处理说明(可选,用于审计记录)。
|
||||
type: string
|
||||
report_ids:
|
||||
description: ReportIDs 待处理举报ID列表。
|
||||
items:
|
||||
type: integer
|
||||
minItems: 1
|
||||
type: array
|
||||
required:
|
||||
- action
|
||||
- report_ids
|
||||
type: object
|
||||
dto.SuperContentReportItem:
|
||||
properties:
|
||||
content_id:
|
||||
@@ -3596,6 +3620,28 @@ paths:
|
||||
summary: Process content report
|
||||
tags:
|
||||
- Content
|
||||
/super/v1/content-reports/process/batch:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Batch process content report records
|
||||
parameters:
|
||||
- description: Batch process form
|
||||
in: body
|
||||
name: form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SuperContentReportBatchProcessForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Processed
|
||||
schema:
|
||||
type: string
|
||||
summary: Batch process content reports
|
||||
tags:
|
||||
- Content
|
||||
/super/v1/contents:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 1) 总体结论
|
||||
|
||||
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、钱包流水与异常排查、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)、审计日志与系统配置、评论治理。
|
||||
- **部分落地**:内容治理(缺批量处置扩展)、创作者治理(缺提现审核联动与结算账户审批流)。
|
||||
- **部分落地**:暂无。
|
||||
- **未落地**:暂无。
|
||||
|
||||
## 2) 按页面完成度(对照 2.x)
|
||||
@@ -51,9 +51,9 @@
|
||||
- 缺口:无显著功能缺口。
|
||||
|
||||
### 2.9 创作者与成员审核 `/superadmin/creators`
|
||||
- 状态:**部分完成**
|
||||
- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除。
|
||||
- 缺口:结算账户审批流(若需要区分通过/驳回状态)。
|
||||
- 状态:**已完成**
|
||||
- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除、结算账户审核。
|
||||
- 缺口:无显著功能缺口。
|
||||
|
||||
### 2.10 优惠券 `/superadmin/coupons`
|
||||
- 状态:**已完成**
|
||||
@@ -88,9 +88,8 @@
|
||||
## 3) `/super/v1` 接口覆盖度概览
|
||||
|
||||
- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents(含评论治理)、Orders、Withdrawals、Finance(流水/异常)、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。
|
||||
- **缺失/待补**:内容治理批量处置扩展(举报/下架/封禁等批量操作)。
|
||||
- **缺失/待补**:无。
|
||||
|
||||
## 4) 建议的下一步(按优先级)
|
||||
|
||||
1. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理。
|
||||
2. **创作者治理补强**:结算账户审批流、提现审核联动流程。
|
||||
暂无。
|
||||
|
||||
@@ -276,5 +276,19 @@ export const ContentService = {
|
||||
reason
|
||||
}
|
||||
});
|
||||
},
|
||||
async batchProcessContentReports({ report_ids, action, content_action, reason } = {}) {
|
||||
if (!Array.isArray(report_ids) || report_ids.length === 0) {
|
||||
throw new Error('report_ids is required');
|
||||
}
|
||||
return requestJson('/super/v1/content-reports/process/batch', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
report_ids,
|
||||
action,
|
||||
content_action,
|
||||
reason
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,6 +79,7 @@ const reportHandledAtFrom = ref(null);
|
||||
const reportHandledAtTo = ref(null);
|
||||
const reportSortField = ref('created_at');
|
||||
const reportSortOrder = ref(-1);
|
||||
const selectedReports = ref([]);
|
||||
|
||||
const commentDeleteDialogVisible = ref(false);
|
||||
const commentDeleteLoading = ref(false);
|
||||
@@ -92,6 +93,13 @@ const reportProcessContentAction = ref('unpublish');
|
||||
const reportProcessReason = ref('');
|
||||
const reportProcessTarget = ref(null);
|
||||
|
||||
const reportBatchDialogVisible = ref(false);
|
||||
const reportBatchSubmitting = ref(false);
|
||||
const reportBatchAction = ref('approve');
|
||||
const reportBatchContentAction = ref('unpublish');
|
||||
const reportBatchReason = ref('');
|
||||
const reportBatchTargetIDs = ref([]);
|
||||
|
||||
const reviewDialogVisible = ref(false);
|
||||
const reviewSubmitting = ref(false);
|
||||
const reviewAction = ref('approve');
|
||||
@@ -176,6 +184,11 @@ function getContentID(row) {
|
||||
return Number.isFinite(id) ? id : 0;
|
||||
}
|
||||
|
||||
function getReportID(row) {
|
||||
const id = Number(row?.id ?? 0);
|
||||
return Number.isFinite(id) ? id : 0;
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-';
|
||||
if (String(value).startsWith('0001-01-01')) return '-';
|
||||
@@ -369,9 +382,12 @@ function getReportActionLabel(value) {
|
||||
}
|
||||
|
||||
const selectedCount = computed(() => selectedContents.value.length);
|
||||
const reportSelectedCount = computed(() => selectedReports.value.length);
|
||||
const reviewTargetCount = computed(() => reviewTargetIDs.value.length);
|
||||
const batchStatusTargetCount = computed(() => batchStatusTargetIDs.value.length);
|
||||
const reportProcessNeedsContentAction = computed(() => reportProcessAction.value === 'approve');
|
||||
const reportBatchTargetCount = computed(() => reportBatchTargetIDs.value.length);
|
||||
const reportBatchNeedsContentAction = computed(() => reportBatchAction.value === 'approve');
|
||||
|
||||
async function loadContents() {
|
||||
loading.value = true;
|
||||
@@ -632,6 +648,7 @@ async function loadReports() {
|
||||
sortOrder: reportSortOrder.value
|
||||
});
|
||||
reports.value = result.items;
|
||||
selectedReports.value = [];
|
||||
reportTotalRecords.value = result.total;
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载举报列表', life: 4000 });
|
||||
@@ -699,6 +716,60 @@ async function confirmReportProcess() {
|
||||
}
|
||||
}
|
||||
|
||||
function openReportBatchDialog(action) {
|
||||
if (!selectedReports.value.length) {
|
||||
toast.add({ severity: 'warn', summary: '请先选择举报', detail: '至少选择 1 条举报进行处理', life: 3000 });
|
||||
return;
|
||||
}
|
||||
const invalid = selectedReports.value.filter((row) => row?.status !== 'pending');
|
||||
if (invalid.length) {
|
||||
toast.add({ severity: 'warn', summary: '选择包含已处理举报', detail: '仅可批量处理待处理举报', life: 3000 });
|
||||
return;
|
||||
}
|
||||
const ids = selectedReports.value.map((row) => getReportID(row)).filter((id) => id > 0);
|
||||
if (!ids.length) {
|
||||
toast.add({ severity: 'warn', summary: '选择无效', detail: '未识别到可处理的举报', life: 3000 });
|
||||
return;
|
||||
}
|
||||
reportBatchTargetIDs.value = ids;
|
||||
reportBatchAction.value = action || 'approve';
|
||||
reportBatchContentAction.value = 'unpublish';
|
||||
reportBatchReason.value = '';
|
||||
reportBatchDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmReportBatchProcess() {
|
||||
const ids = reportBatchTargetIDs.value.filter((id) => id > 0);
|
||||
if (!ids.length) return;
|
||||
|
||||
const action = reportBatchAction.value;
|
||||
const contentAction = action === 'approve' ? reportBatchContentAction.value : 'ignore';
|
||||
const reason = reportBatchReason.value.trim();
|
||||
if (action === 'reject' && !reason) {
|
||||
toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回举报时需填写原因', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
reportBatchSubmitting.value = true;
|
||||
try {
|
||||
await ContentService.batchProcessContentReports({
|
||||
report_ids: ids,
|
||||
action,
|
||||
content_action: contentAction,
|
||||
reason: reason || undefined
|
||||
});
|
||||
toast.add({ severity: 'success', summary: '已处理', detail: `已处理 ${ids.length} 条举报`, life: 3000 });
|
||||
reportBatchDialogVisible.value = false;
|
||||
reportBatchTargetIDs.value = [];
|
||||
selectedReports.value = [];
|
||||
await loadReports();
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '处理失败', detail: error?.message || '无法处理举报', life: 4000 });
|
||||
} finally {
|
||||
reportBatchSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const unpublishDialogVisible = ref(false);
|
||||
const unpublishLoading = ref(false);
|
||||
const unpublishItem = ref(null);
|
||||
@@ -1072,7 +1143,12 @@ watch(
|
||||
<span class="font-medium">举报列表</span>
|
||||
<span class="text-muted-color">跨租户内容举报处理</span>
|
||||
</div>
|
||||
<Button label="刷新" icon="pi pi-refresh" severity="secondary" @click="loadReports" :disabled="reportLoading" />
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-color">已选 {{ reportSelectedCount }} 条</span>
|
||||
<Button label="批量通过" icon="pi pi-check" severity="success" :disabled="reportSelectedCount === 0" @click="openReportBatchDialog('approve')" />
|
||||
<Button label="批量驳回" icon="pi pi-times" severity="danger" :disabled="reportSelectedCount === 0" @click="openReportBatchDialog('reject')" />
|
||||
<Button label="刷新" icon="pi pi-refresh" severity="secondary" @click="loadReports" :disabled="reportLoading" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -1133,6 +1209,7 @@ watch(
|
||||
<DataTable
|
||||
:value="reports"
|
||||
dataKey="id"
|
||||
v-model:selection="selectedReports"
|
||||
:loading="reportLoading"
|
||||
lazy
|
||||
:paginator="true"
|
||||
@@ -1151,6 +1228,7 @@ watch(
|
||||
scrollHeight="640px"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column selectionMode="multiple" headerStyle="width: 3rem" />
|
||||
<Column field="id" header="举报ID" sortable style="min-width: 8rem" />
|
||||
<Column header="内容" style="min-width: 20rem">
|
||||
<template #body="{ data }">
|
||||
@@ -1326,6 +1404,34 @@ watch(
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="reportBatchDialogVisible" :modal="true" :style="{ width: '560px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">批量处理举报</span>
|
||||
<span class="text-muted-color">共 {{ reportBatchTargetCount }} 条</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label class="block font-medium mb-2">处理动作</label>
|
||||
<Select v-model="reportBatchAction" :options="reportActionOptions" optionLabel="label" optionValue="value" class="w-full" />
|
||||
</div>
|
||||
<div v-if="reportBatchNeedsContentAction">
|
||||
<label class="block font-medium mb-2">内容处置</label>
|
||||
<Select v-model="reportBatchContentAction" :options="reportContentActionOptions" optionLabel="label" optionValue="value" class="w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block font-medium mb-2">处理说明</label>
|
||||
<InputText v-model="reportBatchReason" placeholder="可选,便于审计与通知" class="w-full" />
|
||||
</div>
|
||||
<div class="text-sm text-muted-color">批量处理后会记录审计,并视情况通知作者。</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="取消" icon="pi pi-times" text @click="reportBatchDialogVisible = false" :disabled="reportBatchSubmitting" />
|
||||
<Button label="确认处理" icon="pi pi-check" severity="success" @click="confirmReportBatchProcess" :loading="reportBatchSubmitting" :disabled="reportBatchSubmitting || (reportBatchAction === 'reject' && !reportBatchReason.trim())" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="commentDeleteDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user